From 2279662a11b95f90fc4f4f1b28e7ba78dc8e8f77 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 5 Mar 2026 20:55:34 +0100 Subject: [PATCH 001/173] add initial version --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 1636 ++++++++++++++++++++++ wled00/src/WLEDpixelBus/WLEDpixelBus.h | 0 2 files changed, 1636 insertions(+) create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus.h diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp new file mode 100644 index 0000000000..d0f4fed1ae --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -0,0 +1,1636 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +by @dedehai, 2026 + +Features: +- Runtime LED timing configuration +- Double-buffered DMA with interrupt-driven refilling (4-step cadence) +- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 +- RMT, I2S parallel, and LCD parallel output methods +- RGBW uint32_t pixel buffer format (WLED native) +- Separate CCT (WW/CW) buffer support +- IDF v4.x compatible + +-------------------------------------------------------------------------*/ + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +//============================================================================== +// Color Encoder Implementation +//============================================================================== + +void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const { + uint8_t r = getR(pixel); + uint8_t g = getG(pixel); + uint8_t b = getB(pixel); + uint8_t w = getW(pixel); + + // For CCT strips, use CCT data instead of W channel + uint8_t ww = cct ? cct->ww : w; + uint8_t cw = cct ? cct->cw : 0; + + switch (_order) { + // RGB variants + case ColorOrder::RGB: out[0]=r; out[1]=g; out[2]=b; break; + case ColorOrder::GRB: out[0]=g; out[1]=r; out[2]=b; break; + case ColorOrder::BRG: out[0]=b; out[1]=r; out[2]=g; break; + case ColorOrder::RBG: out[0]=r; out[1]=b; out[2]=g; break; + case ColorOrder::GBR: out[0]=g; out[1]=b; out[2]=r; break; + case ColorOrder::BGR: out[0]=b; out[1]=g; out[2]=r; break; + + // RGBW variants + case ColorOrder::RGBW: out[0]=r; out[1]=g; out[2]=b; out[3]=w; break; + case ColorOrder::GRBW: out[0]=g; out[1]=r; out[2]=b; out[3]=w; break; + case ColorOrder::BRGW: out[0]=b; out[1]=r; out[2]=g; out[3]=w; break; + case ColorOrder::RBGW: out[0]=r; out[1]=b; out[2]=g; out[3]=w; break; + case ColorOrder::GBRW: out[0]=g; out[1]=b; out[2]=r; out[3]=w; break; + case ColorOrder::BGRW: out[0]=b; out[1]=g; out[2]=r; out[3]=w; break; + case ColorOrder::WRGB: out[0]=w; out[1]=r; out[2]=g; out[3]=b; break; + case ColorOrder::WGRB: out[0]=w; out[1]=g; out[2]=r; out[3]=b; break; + case ColorOrder::WBRG: out[0]=w; out[1]=b; out[2]=r; out[3]=g; break; + case ColorOrder::WRBG: out[0]=w; out[1]=r; out[2]=b; out[3]=g; break; + case ColorOrder::WGBR: out[0]=w; out[1]=g; out[2]=b; out[3]=r; break; + case ColorOrder::WBGR: out[0]=w; out[1]=b; out[2]=g; out[3]=r; break; + + default: out[0]=g; out[1]=r; out[2]=b; break; + } +} + +//============================================================================== +// RMT Bus Implementation +//============================================================================== + +// Thread-local context for RMT translate callback +static struct { + uint32_t bit0; + uint32_t bit1; +} s_rmtCtx; + +RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) + : _pin(pin) + , _channel(channel) + , _timing(timing) + , _order(order) + , _inverted(false) + , _initialized(false) + , _rmtChannel(RMT_CHANNEL_0) + , _rmtBit0(0) + , _rmtBit1(0) + , _rmtResetTicks(0) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) +{ +} + +RmtBus::~RmtBus() { + end(); +} + +void RmtBus::updateRmtTiming() { + // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick + const uint8_t clockDiv = 2; + 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; + }; + + uint16_t t0h = nsToTicks(_timing.t0h_ns); + uint16_t t0l = nsToTicks(_timing.t0l_ns); + uint16_t t1h = nsToTicks(_timing.t1h_ns); + uint16_t t1l = nsToTicks(_timing.t1l_ns); + + rmt_item32_t bit0, bit1; + + if (_inverted) { + bit0.level0 = 0; bit0.duration0 = t0h; + bit0.level1 = 1; bit0.duration1 = t0l; + bit1.level0 = 0; bit1.duration0 = t1h; + bit1.level1 = 1; bit1.duration1 = t1l; + } else { + bit0.level0 = 1; bit0.duration0 = t0h; + bit0.level1 = 0; bit0.duration1 = t0l; + bit1.level0 = 1; bit1.duration0 = t1h; + bit1.level1 = 0; bit1.duration1 = t1l; + } + + _rmtBit0 = bit0.val; + _rmtBit1 = bit1.val; + _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); +} + +bool RmtBus::begin() { + if (_initialized) return true; + + // Auto-select channel if needed + if (_channel < 0) { + _channel = 0; + } + +#if defined(WLEDPB_ESP32) + if (_channel > 7) return false; +#else + if (_channel > 3) return false; +#endif + _rmtChannel = (rmt_channel_t)_channel; + + updateRmtTiming(); + + rmt_config_t config = {}; + + config.rmt_mode = RMT_MODE_TX; + config.channel = _rmtChannel; + config.gpio_num = (gpio_num_t)_pin; + config.mem_block_num = 1; + config.clk_div = 2; // 40MHz + + config.tx_config.loop_en = false; + config.tx_config.carrier_en = false; + //config.tx_config.carrier_freq_hz = 38000; + //config.tx_config.carrier_duty_percent = 33; + config.tx_config.idle_output_en = true; + config.tx_config.idle_level = _inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW; + + + esp_err_t err = rmt_config(&config); + if (err != ESP_OK) { + log_e("RMT config failed: %d", err); + return false; + } + + + err = rmt_driver_install(_rmtChannel, 0, ESP_INTR_FLAG_IRAM); + if (err != ESP_OK) { + log_e("RMT driver install failed: %d", err); + return false; + } + + err = rmt_translator_init(_rmtChannel, translateCB); + if (err != ESP_OK) { + log_e("RMT translator init failed: %d", err); + rmt_driver_uninstall(_rmtChannel); + return false; + } + + _initialized = true; + return true; +} + +void RmtBus::end() { + if (!_initialized) return; + + rmt_driver_uninstall(_rmtChannel); + + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool RmtBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + } + + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!_initialized || !pixels || numPixels == 0) return false; + + // Wait for previous transmission + rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + + if (!allocateBuffer(numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + dst += numCh; + } + + // Update context for ISR + s_rmtCtx.bit0 = _rmtBit0; + s_rmtCtx.bit1 = _rmtBit1; + + // Start transmission + size_t dataLen = numPixels * numCh; + esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, true); + + return err == ESP_OK; +} + +bool RmtBus::canShow() const { + if (!_initialized) return true; + + rmt_channel_status_result_t status; + rmt_get_channel_status(&status); + return status.status[_rmtChannel] == RMT_CHANNEL_IDLE; +} + +void RmtBus::waitComplete() { + if (_initialized) { + rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + } +} + +void RmtBus::setTiming(const LedTiming& timing) { + _timing = timing; + if (_initialized) { + updateRmtTiming(); + } +} + +void RmtBus::setColorOrder(ColorOrder order) { + _order = order; +} + +void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num) { + if (src == nullptr || dest == nullptr) { + *translated_size = 0; + *item_num = 0; + return; + } + + const uint8_t* psrc = (const uint8_t*)src; + rmt_item32_t* pdest = dest; + size_t size = 0; + size_t num = 0; + + uint32_t bit0 = s_rmtCtx.bit0; + uint32_t bit1 = s_rmtCtx.bit1; + + while (size < src_size && num < wanted_num) { + uint8_t byte = *psrc++; + size++; + + for (int i = 7; i >= 0 && num < wanted_num; i--) { + pdest->val = (byte & (1 << i)) ? bit1 : bit0; + pdest++; + num++; + } + } + + *translated_size = size; + *item_num = num; +} + +//============================================================================== +// I2S Bus Implementation +//============================================================================== + +#ifdef WLEDPB_I2S_SUPPORT + +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); + } + _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; + } +} + +I2sBusContext::I2sBusContext(uint8_t busNum) + : _busNum(busNum) + , _state(DriverState::Idle) + , _initialized(false) + , _bufferSize(0) + , _activeBuffer(0) + , _timing{0, 0, 0, 0, 0} + , _clockDiv(1) + , _isrHandle(nullptr) + , _channelCount(0) + , _channelMask(0) + , _maxDataLen(0) +{ +#if defined(WLEDPB_ESP32) + _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; +#else + _i2sDev = &I2S0; +#endif + + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + + _dmaDesc[0] = _dmaDesc[1] = nullptr; + _dmaBuffer[0] = _dmaBuffer[1] = nullptr; +} + +I2sBusContext:: ~I2sBusContext() { + deinit(); +} + +bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { + if (_initialized) return true; + + _timing = timing; + _bufferSize = bufferSize; + + // Allocate DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_malloc(bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + log_e("I2S DMA buffer alloc failed"); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, bufferSize); + + _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + log_e("I2S DMA desc alloc failed"); + deinit(); + return false; + } + } + + // Setup DMA descriptors - linked list for ping-pong + for (int i = 0; i < 2; i++) { + _dmaDesc[i]->size = bufferSize; + _dmaDesc[i]->length = bufferSize; + _dmaDesc[i]->buf = _dmaBuffer[i]; + _dmaDesc[i]->eof = 1; // Generate interrupt on completion + _dmaDesc[i]->sosf = 0; + _dmaDesc[i]->owner = 1; + _dmaDesc[i]->qe.stqe_next = nullptr; // Set dynamically + } + + // Enable I2S peripheral +#if defined(WLEDPB_ESP32) + periph_module_enable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); +#else + periph_module_enable(PERIPH_I2S0_MODULE); +#endif + + // Reset I2S + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.rx_reset = 1; + _i2sDev->conf.rx_reset = 0; + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_fifo_reset = 0; + + // Configure for parallel LCD mode + _i2sDev->conf2.lcd_en = 1; + _i2sDev->conf2.lcd_tx_wrx2_en = 0; + _i2sDev->conf2.lcd_tx_sdx2_en = 0; + + // Calculate clock divider for 4-step cadence + // Bit period / 4 = step time + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t stepPeriodNs = bitPeriodNs / 4; + +#if defined(WLEDPB_ESP32) + uint32_t baseClock = 160000000; +#else + uint32_t baseClock = 80000000; +#endif + + _clockDiv = (baseClock / 1000000) * stepPeriodNs / 1000; + if (_clockDiv < 2) _clockDiv = 2; + if (_clockDiv > 255) _clockDiv = 255; + + // Set clock + _i2sDev->clkm_conf.clkm_div_a = 0; + _i2sDev->clkm_conf.clkm_div_b = 0; + _i2sDev->clkm_conf.clkm_div_num = _clockDiv; + _i2sDev->clkm_conf.clk_en = 1; + + // Sample configuration + _i2sDev->fifo_conf.tx_fifo_mod = 1; // 8-bit parallel + _i2sDev->fifo_conf.tx_fifo_mod_force_en = 1; + _i2sDev->sample_rate_conf.tx_bck_div_num = 1; + _i2sDev->sample_rate_conf.tx_bits_mod = 8; + + // Channel config + _i2sDev->conf_chan.tx_chan_mod = 1; + _i2sDev->conf.tx_right_first = 1; + _i2sDev->conf.tx_msb_right = 0; + _i2sDev->conf.tx_mono = 1; + + // Enable DMA + _i2sDev->fifo_conf.dscr_en = 1; + _i2sDev->lc_conf.out_data_burst_en = 1; + _i2sDev->lc_conf.outdscr_burst_en = 1; + + // Install ISR + int intSource; +#if defined(WLEDPB_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_LEVEL1, + dmaISR, this, &_isrHandle); + if (err != ESP_OK) { + log_e("I2S ISR alloc failed: %d", err); + deinit(); + return false; + } + + _initialized = true; + return true; +} + +void I2sBusContext::deinit() { + if (_i2sDev) { + _i2sDev->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } + + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; + } + } + +#if defined(WLEDPB_ESP32) + periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); +#else + periph_module_disable(PERIPH_I2S0_MODULE); +#endif + + _initialized = false; +} + +int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { + // Find free slot + int8_t idx = -1; + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + if (! _channels[i].active) { + idx = i; + break; + } + } + + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + + // Configure GPIO + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + + // Route I2S output to GPIO + int sigIdx; +#if defined(WLEDPB_ESP32) + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; +#else + sigIdx = I2S0O_DATA_OUT0_IDX; +#endif + sigIdx += idx; + + gpio_matrix_out(pin, sigIdx, false, false); + + return idx; +} + +void I2sBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + if (len > _maxDataLen) { + _maxDataLen = len; + } +} + +void I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, size_t* bytesWritten) { + // 4-step cadence encoding for parallel output + // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) + // Pattern: [1][data][data][0] for each bit + // + // For multiple channels, we interleave bits into each byte position + + size_t written = 0; + memset(dest, 0, destLen); + + // Process each source byte position across all channels + while (written + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte + bool hasData = false; + + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (! _channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + hasData = true; + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint8_t chMask = (1 << ch); + + uint8_t* p = dest + written; + for (int bit = 7; bit >= 0; bit--) { + uint8_t dataVal = (srcByte >> bit) & 1; + + // Step 0: Always HIGH (start of bit) + p[0] |= chMask; + + // Step 1: HIGH for '1', LOW for '0' + if (dataVal) p[1] |= chMask; + + // Step 2: HIGH for '1', LOW for '0' + if (dataVal) p[2] |= chMask; + + // Step 3: Always LOW (already 0) + + p += 4; + } + } + + if (!hasData) break; + + // Advance all channel positions + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } + } + + written += 32; + } + + *bytesWritten = written; +} + +void I2sBusContext:: fillBuffer(uint8_t bufIdx) { + size_t written; + encode4Step(_dmaBuffer[bufIdx], _bufferSize, &written); + _dmaDesc[bufIdx]->length = written; +} + +bool I2sBusContext::startTransmit() { + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; + + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + // Fill both buffers initially + fillBuffer(0); + fillBuffer(1); + + // Check if we need second buffer + bool moreData = false; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + moreData = true; + break; + } + } + + // Setup DMA chain + if (moreData) { + _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; + _dmaDesc[1]->qe.stqe_next = nullptr; // Will be set in ISR if more data + } else { + _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]->length > 0 ? _dmaDesc[1] : nullptr; + _dmaDesc[1]->qe.stqe_next = nullptr; + } + + _activeBuffer = 0; + _state = DriverState::Sending; + + // Reset FIFO + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_fifo_reset = 0; + + // Clear interrupts + _i2sDev->int_clr.val = 0xFFFFFFFF; + + // Enable EOF interrupt + _i2sDev->int_ena.out_eof = 1; + + // Set DMA start address + _i2sDev->out_link.addr = (uint32_t)_dmaDesc[0]; + + // Start transmission + _i2sDev->out_link.start = 1; + _i2sDev->conf.tx_start = 1; + + return true; +} + +void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { + I2sBusContext* ctx = (I2sBusContext*)arg; + i2s_dev_t* dev = ctx->_i2sDev; + + uint32_t status = dev->int_st.val; + dev->int_clr.val = status; + + if (status & I2S_OUT_EOF_INT_ST) { + // Current buffer finished, check if more data + bool moreData = false; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (ctx->_channels[ch].active && + ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { + moreData = true; + break; + } + } + + if (moreData) { + // Refill the completed buffer + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; // Switch to other buffer + + // Fill the completed buffer with new data + size_t written; + ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize, &written); + ctx->_dmaDesc[completedBuf]->length = written; + + // Link it after current buffer + ctx->_dmaDesc[ctx->_activeBuffer]->qe.stqe_next = ctx->_dmaDesc[completedBuf]; + ctx->_dmaDesc[completedBuf]->qe.stqe_next = nullptr; + } else { + // All data sent, add reset time then go idle + ctx->_state = DriverState::Idle; + dev->conf.tx_start = 0; + dev->out_link.start = 0; + } + } +} + +// I2sBus implementation + +I2sBus::I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, + uint8_t busNum, size_t bufferSize) + : _pin(pin) + , _busNum(busNum) + , _bufferSize(bufferSize) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) + , _encodedLen(0) +{ +} + +I2sBus::~I2sBus() { + end(); +} + +bool I2sBus::begin() { + if (_initialized) return true; + + _ctx = I2sBusContext::get(_busNum); + if (!_ctx) return false; + + if (!_ctx->init(_timing, _bufferSize)) { + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _initialized = true; + return true; +} + +void I2sBus::end() { + if (!_initialized) return; + + if (_ctx) { + _ctx->unregisterChannel(_channelIdx); + I2sBusContext::release(_busNum); + _ctx = nullptr; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool I2sBus:: allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + } + + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!_initialized || !_ctx || !pixels || numPixels == 0) return false; + + // Wait for previous transmission + while (! _ctx->isIdle()) { + vTaskDelay(1); + } + + if (!allocateBuffer(numPixels)) return false; + + // Encode pixels + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + dst += numCh; + } + + _encodedLen = numPixels * numCh; + + // Set data for our channel + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); + + // Start transmission + return _ctx->startTransmit(); +} + +bool I2sBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void I2sBus::waitComplete() { + while (_ctx && !_ctx->isIdle()) { + vTaskDelay(1); + } +} + +void I2sBus::setColorOrder(ColorOrder order) { + _order = order; +} + +#endif // WLEDPB_I2S_SUPPORT + +//============================================================================== +// LCD Bus Implementation (ESP32-S3) +//============================================================================== +/*------------------------------------------------------------------------- +WLEDpixelBus - LCD Implementation with Continuous DMA (Glitch-Free) + +Key design: +- DMA runs continuously in circular mode (buf0 -> buf1 -> buf0 -> ...) +- Buffers are filled with data or zeros (for reset period) +- Stop only after reset period completes on buffer boundary +- No DMA reconfiguration during transmission +-------------------------------------------------------------------------*/ + +#ifdef WLEDPB_LCD_SUPPORT + +#include "driver/periph_ctrl.h" +#include "esp_private/gdma.h" +#include "esp_rom_gpio.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" + +// ============================================ +// Configuration +// ============================================ + +#ifndef WLEDPB_LCD_DMA_BUFFER_SIZE +#define WLEDPB_LCD_DMA_BUFFER_SIZE 512 +#endif + +#ifndef WLEDPB_LCD_CADENCE_STEPS +#define WLEDPB_LCD_CADENCE_STEPS 4 +#endif + +#ifndef WLEDPB_LCD_DEBUG +#define WLEDPB_LCD_DEBUG 1 +#endif + +#if WLEDPB_LCD_DEBUG + #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) +#else + #define LCD_LOG(fmt, ...) +#endif + +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); +static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); + + +// Debug counters +static volatile uint32_t s_isrCount = 0; +static volatile uint32_t s_dataFills = 0; +static volatile uint32_t s_zeroFills = 0; + +LcdBusContext* LcdBusContext::_instance = nullptr; +uint8_t LcdBusContext::_refCount = 0; + +LcdBusContext* LcdBusContext::get() { + if (_instance == nullptr) { + _instance = new LcdBusContext(); + } + _refCount++; + return _instance; +} + +void LcdBusContext::release() { + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } +} + +LcdBusContext::LcdBusContext() + : _state(DriverState::Idle) + , _txState(TxState::Idle) + , _initialized(false) + , _use16Bit(false) + , _dmaChannel(nullptr) + , _timing{0, 0, 0, 0, 0} + , _channelCount(0) + , _channelMask(0) + , _maxDataLen(0) + , _activeBuffer(0) + , _resetBytesRemaining(0) + , _resetBytesPerBuffer(0) +{ + for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + for (int i = 0; i < 2; i++) { + _dmaDesc[i] = nullptr; + _dmaBuffer[i] = nullptr; + } +} + +LcdBusContext::~LcdBusContext() { + deinit(); +} + +bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16Bit) { + if (_initialized) { + LCD_LOG("Already initialized"); + return true; + } + + LCD_LOG("Initializing LCD bus context (continuous DMA mode)"); + LCD_LOG(" DMA buffer size: %u bytes x2", WLEDPB_LCD_DMA_BUFFER_SIZE); + LCD_LOG(" Cadence: %d-step", WLEDPB_LCD_CADENCE_STEPS); + + _timing = timing; + _use16Bit = use16Bit; + + // Calculate reset bytes needed + // Reset time in us -> bytes at our clock rate + // Clock period = bitPeriod / cadenceSteps + // Bytes per us = 1000 / clockPeriodNs + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t clockPeriodNs = bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS; + uint32_t bytesPerUs = 1000 / clockPeriodNs; + _resetBytesPerBuffer = timing.reset_us * bytesPerUs; + + // Round up to buffer size for clean stopping + if (_resetBytesPerBuffer < WLEDPB_LCD_DMA_BUFFER_SIZE) { + _resetBytesPerBuffer = WLEDPB_LCD_DMA_BUFFER_SIZE; + } + + LCD_LOG(" Reset time: %u us = %u bytes minimum", timing.reset_us, _resetBytesPerBuffer); + + // Allocate double DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_malloc(WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + LCD_LOG("ERROR: DMA buffer %d alloc failed", i); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); + + _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + LCD_LOG("ERROR: DMA desc %d alloc failed", i); + deinit(); + return false; + } + memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); + } + + // Setup DMA descriptors in CIRCULAR mode (never changes during operation!) + // buf0 -> buf1 -> buf0 -> buf1 -> ... + for (int i = 0; i < 2; i++) { + _dmaDesc[i]->dw0.size = WLEDPB_LCD_DMA_BUFFER_SIZE; + _dmaDesc[i]->dw0.length = WLEDPB_LCD_DMA_BUFFER_SIZE; + _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR + _dmaDesc[i]->buffer = _dmaBuffer[i]; + _dmaDesc[i]->next = _dmaDesc[i ^ 1]; // Point to other buffer (circular) + } + + LCD_LOG(" DMA descriptors configured in circular mode"); + + // 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 + double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; + + LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); + + if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { + LCD_LOG("ERROR: Invalid clock divider"); + deinit(); + 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; + LCD_CAM.lcd_user.lcd_2byte_en = use16Bit ? 1 : 0; + 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) { + LCD_LOG("ERROR: GDMA alloc failed: %d", err); + deinit(); + return false; + } + + err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + if (err != ESP_OK) { + LCD_LOG("ERROR: GDMA connect failed: %d", err); + deinit(); + return false; + } + + gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(_dmaChannel, &strategy_config); + + // Register DMA callback + gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; + gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); + + _initialized = true; + LCD_LOG("LCD bus initialized (continuous circular DMA)"); + + return true; +} + +void LcdBusContext::deinit() { + LCD_LOG("Deinitializing LCD"); + + // Stop transmission + LCD_CAM.lcd_user.lcd_start = 0; + _state = DriverState::Idle; + _txState = TxState::Idle; + + if (_dmaChannel) { + gdma_stop(_dmaChannel); + gdma_disconnect(_dmaChannel); + gdma_del_channel(_dmaChannel); + _dmaChannel = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; + } + } + + if (_initialized) { + periph_module_disable(PERIPH_LCD_CAM_MODULE); + } + + _initialized = false; +} + +int8_t LcdBusContext:: registerChannel(int8_t pin, LcdBus* bus) { + LCD_LOG("Registering channel: pin=%d", pin); + + int8_t idx = -1; + for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { + if (! _channels[i].active) { + idx = i; + break; + } + } + + if (idx < 0) { + LCD_LOG("ERROR: No free channels"); + return -1; + } + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + + uint8_t muxIdx = LCD_DATA_OUT0_IDX + idx; + esp_rom_gpio_connect_out_signal(pin, muxIdx, false, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); + + LCD_LOG(" Channel %d registered: pin=%d", idx, pin); + return idx; +} + +void LcdBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_matrix_out(_channels[channelIdx].pin, SIG_GPIO_OUT_IDX, false, false); + pinMode(_channels[channelIdx].pin, INPUT); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void LcdBusContext:: setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + if (len > _maxDataLen) { + _maxDataLen = len; + } +} + +size_t LcdBusContext:: encodeIntoBuffer(uint8_t* dest, size_t destLen) { + // Encode pixel data from all channels + // Returns number of bytes written (0 if no more data) + + size_t written = 0; + const size_t bytesPerSourceByte = 8 * WLEDPB_LCD_CADENCE_STEPS; + + // Clear buffer (important for OR operations and zero-fill) + memset(dest, 0, destLen); + + while (written + bytesPerSourceByte <= destLen) { + // Check if any channel has data + bool hasData = false; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + hasData = true; + break; + } + } + + if (! hasData) break; + + // Encode one byte from each active channel + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (! _channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint8_t chMask = (1 << ch); + uint8_t* p = dest + written; + + for (int bit = 7; bit >= 0; bit--) { + uint8_t dataVal = (srcByte >> bit) & 1; + +#if WLEDPB_LCD_CADENCE_STEPS == 4 + p[0] |= chMask; + if (dataVal) { + p[1] |= chMask; + p[2] |= chMask; + } + p += 4; +#else + p[0] |= chMask; + if (dataVal) { + p[1] |= chMask; + } + p += 3; +#endif + } + } + + // Advance all channels + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } + } + + written += bytesPerSourceByte; + } + + return written; +} + +bool LcdBusContext::hasMoreData() const { + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + return true; + } + } + return false; +} + +void LcdBusContext::fillBufferForState(uint8_t bufIdx) { + // Fill buffer based on current transmission state + // This is called from ISR context! + + switch (_txState) { + case TxState::SendingData: { + size_t encoded = encodeIntoBuffer(_dmaBuffer[bufIdx], WLEDPB_LCD_DMA_BUFFER_SIZE); + + if (encoded > 0) { + s_dataFills++; + // Still have data, continue + if (! hasMoreData()) { + // This was the last data, switch to reset phase + _txState = TxState::SendingReset; + _resetBytesRemaining = _resetBytesPerBuffer; + } + } else { + // No data encoded, switch to reset + _txState = TxState::SendingReset; + _resetBytesRemaining = _resetBytesPerBuffer; + s_zeroFills++; + // Buffer already zeroed by encodeIntoBuffer + } + break; + } + + case TxState:: SendingReset: { + // Fill with zeros for reset period + memset(_dmaBuffer[bufIdx], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); + s_zeroFills++; + + if (_resetBytesRemaining <= WLEDPB_LCD_DMA_BUFFER_SIZE) { + // This is the last reset buffer, stop after it completes + _txState = TxState::StopPending; + _resetBytesRemaining = 0; + } else { + _resetBytesRemaining -= WLEDPB_LCD_DMA_BUFFER_SIZE; + } + break; + } + + case TxState::StopPending: { + // Fill with zeros but we'll stop after this + memset(_dmaBuffer[bufIdx], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); + s_zeroFills++; + break; + } + + default: + break; + } +} + +bool LcdBusContext::startTransmit() { + if (_state != DriverState::Idle) { + LCD_LOG("ERROR: Not idle"); + return false; + } + + if (_channelCount == 0) { + LCD_LOG("ERROR: No channels"); + return false; + } + + // Reset channel positions + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + if (_maxDataLen == 0) { + LCD_LOG("ERROR: No data"); + return false; + } + + // Reset debug counters + s_isrCount = 0; + s_dataFills = 0; + s_zeroFills = 0; + + LCD_LOG("Starting transmission: %u bytes", _maxDataLen); + + // Set initial state + _txState = TxState::SendingData; + _activeBuffer = 0; + _resetBytesRemaining = 0; + + // Pre-fill both buffers with data + // Buffer 0: First chunk + size_t len0 = encodeIntoBuffer(_dmaBuffer[0], WLEDPB_LCD_DMA_BUFFER_SIZE); + s_dataFills++; + + if (! hasMoreData()) { + // All data fits in buffer 0, prepare for reset + _txState = TxState::SendingReset; + _resetBytesRemaining = _resetBytesPerBuffer; + } + + // Buffer 1: Second chunk or start of reset + fillBufferForState(1); + + _state = DriverState::Sending; + + // Start DMA (circular mode - descriptors already linked) + // DO NOT call gdma_reset here - just start fresh + 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; + + gdma_start(_dmaChannel, (intptr_t)_dmaDesc[0]); + esp_rom_delay_us(1); + LCD_CAM.lcd_user.lcd_start = 1; + + LCD_LOG(" DMA started in circular mode"); + + return true; +} + +IRAM_ATTR bool LcdBusContext:: dmaCallback(gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data) { + LcdBusContext* ctx = (LcdBusContext*)user_data; + + s_isrCount++; + + // The buffer that just completed is the active one + // We need to refill it while the other buffer is being sent + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; // Switch to other buffer + + if (ctx->_txState == TxState::StopPending) { + // Stop transmission cleanly + LCD_CAM.lcd_user.lcd_start = 0; + ctx->_state = DriverState::Idle; + ctx->_txState = TxState::Idle; + return true; + } + + // Refill the completed buffer for next round + ctx->fillBufferForState(completedBuf); + + // Ensure DMA descriptor is ready (owner = DMA) + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + + return true; +} + +void LcdBusContext::printDebugStats() { + LCD_LOG("Stats: ISR=%u, dataFills=%u, zeroFills=%u", + s_isrCount, s_dataFills, s_zeroFills); +} + +// ============================================ +// LcdBus Implementation +// ============================================ + +LcdBus::LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, + size_t bufferSize, bool use16Bit) + : _pin(pin) + , _bufferSize(bufferSize) + , _use16Bit(use16Bit) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) + , _encodedLen(0) +{ +} + +LcdBus::~LcdBus() { + end(); +} + +bool LcdBus::begin() { + if (_initialized) return true; + + LCD_LOG("LcdBus::begin() pin=%d", _pin); + + _ctx = LcdBusContext::get(); + if (!_ctx) return false; + + if (!_ctx->init(_timing, _bufferSize, _use16Bit)) { + LcdBusContext::release(); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + LcdBusContext::release(); + _ctx = nullptr; + return false; + } + + _initialized = true; + LCD_LOG("LcdBus ready: channel=%d", _channelIdx); + return true; +} + +void LcdBus::end() { + if (!_initialized) return; + + if (_ctx) { + _ctx->unregisterChannel(_channelIdx); + LcdBusContext::release(); + _ctx = nullptr; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool LcdBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } + + if (_encodeBuffer) free(_encodeBuffer); + + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool LcdBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!_initialized || !_ctx || !pixels || numPixels == 0) { + return false; + } + + // Wait for idle + uint32_t timeout = 1000; // Longer timeout for large strips + uint32_t start = millis(); + while (! _ctx->isIdle()) { + if (millis() - start > timeout) { + LCD_LOG("ERROR: Timeout waiting for idle"); + _ctx->forceIdle(); + break; + } + delay(1); + } + + if (!allocateBuffer(numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + dst += numCh; + } + + _encodedLen = numPixels * numCh; + + // Set data for our channel + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); + + // Start transmission + return _ctx->startTransmit(); +} + +bool LcdBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void LcdBus::waitComplete() { + if (!_ctx) return; + + uint32_t start = millis(); + while (!_ctx->isIdle()) { + if (millis() - start > 1000) { + LCD_LOG("waitComplete timeout"); + _ctx->printDebugStats(); + _ctx->forceIdle(); + return; + } + delay(1); + } + +#if WLEDPB_LCD_DEBUG + _ctx->printDebugStats(); +#endif +} + +void LcdBus::setColorOrder(ColorOrder order) { + _order = order; +} + +#endif // WLEDPB_LCD_SUPPORT + +//============================================================================== +// Bus Factory Implementation +//============================================================================== + +IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, + ColorOrder order, size_t bufferSize) { + + if (type == BusType::Auto) { + type = getRecommendedBusType(); + } + + IBus* bus = nullptr; + + switch (type) { + case BusType::RMT: + bus = new RmtBus(pin, timing, order); + break; + +#ifdef WLEDPB_I2S_SUPPORT + case BusType::I2S: + bus = new I2sBus(pin, timing, order, 0, bufferSize); + break; +#endif + +#ifdef WLEDPB_LCD_SUPPORT + case BusType::LCD: + bus = new LcdBus(pin, timing, order, bufferSize); + break; +#endif + + default: + return nullptr; + } + + return bus; +} + +BusType getRecommendedBusType() { +#if defined(WLEDPB_ESP32S3) + return BusType::LCD; // S3 has best LCD support +#elif defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) + return BusType::I2S; // Original and S2 have I2S parallel +#else + return BusType::RMT; // Fallback to RMT +#endif +} + +} // namespace WLEDpixelBus \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h new file mode 100644 index 0000000000..e69de29bb2 From c2f4740aa056717eab01e28f20490b79731331e6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 5 Mar 2026 21:16:13 +0100 Subject: [PATCH 002/173] add initial header --- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 633 +++++++++++++++++++++++++ 1 file changed, 633 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index e69de29bb2..4aec0915ac 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -0,0 +1,633 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +by @dedehai, 2026 + +Features: +- Runtime LED timing configuration +- Double-buffered DMA with interrupt-driven refilling (4-step cadence) +- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 +- RMT, I2S parallel, and LCD parallel output methods +- RGBW uint32_t pixel buffer format (WLED native) +- Separate CCT (WW/CW) buffer support +- IDF v4.x compatible + +-------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#if ! defined(ARDUINO_ARCH_ESP32) +#error "WLEDpixelBus only supports ESP32 platforms" +#endif + +#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" + +// Platform detection +#if defined(CONFIG_IDF_TARGET_ESP32) + #define WLEDPB_ESP32 +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define WLEDPB_ESP32S2 +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + #define WLEDPB_ESP32S3 +#elif defined(CONFIG_IDF_TARGET_ESP32C3) + #define WLEDPB_ESP32C3 +#endif + +// I2S support (ESP32 and S2 only for parallel mode) +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) + #define WLEDPB_I2S_SUPPORT +#endif + +// LCD support (S3 only) +#if defined(WLEDPB_ESP32S3) + #define WLEDPB_LCD_SUPPORT +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// LED Timing Configuration - Runtime configurable +//============================================================================== + +/** + * LED timing parameters in nanoseconds + */ +struct LedTiming { + uint16_t t0h_ns; // '0' bit high time + uint16_t t0l_ns; // '0' bit low time + uint16_t t1h_ns; // '1' bit high time + uint16_t t1l_ns; // '1' bit low time + uint32_t reset_us; // Reset/latch time in microseconds + + constexpr LedTiming(uint16_t t0h, uint16_t t0l, uint16_t t1h, uint16_t t1l, uint32_t reset) + : t0h_ns(t0h), t0l_ns(t0l), t1h_ns(t1h), t1l_ns(t1l), reset_us(reset) {} + + // Calculate bit period in ns + constexpr uint32_t bitPeriod() const { + return (t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2; + } +}; + +// Predefined timing constants +namespace Timing { + constexpr LedTiming WS2812 {200, 750, 500, 450, 100}; + constexpr LedTiming WS2811 {300, 950, 900, 350, 300}; + constexpr LedTiming WS2813 {400, 850, 800, 450, 300}; + constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; + constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; + constexpr LedTiming SK6812 {400, 850, 800, 450, 80}; + constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; + constexpr LedTiming TM1829 {300, 900, 800, 400, 200}; + constexpr LedTiming APA106 {350, 1350, 1350, 350, 50}; + constexpr LedTiming UCS1903 {400, 850, 800, 450, 500}; + constexpr LedTiming SM16703 {300, 900, 900, 300, 80}; + constexpr LedTiming Generic800Kbps {400, 850, 800, 450, 300}; + constexpr LedTiming Generic400Kbps {800, 1700, 1600, 900, 300}; +} + +//============================================================================== +// Color Order Configuration +//============================================================================== + +enum class ColorOrder : uint8_t { + // RGB variants (3 bytes) + GRB = 0, + RGB = 1, + BRG = 2, + RBG = 3, + GBR = 4, + BGR = 5, + // RGBW variants (4 bytes) + GRBW = 10, + RGBW = 11, + BRGW = 12, + RBGW = 13, + GBRW = 14, + BGRW = 15, + WRGB = 16, + WGRB = 17, + WBRG = 18, + WRBG = 19, + WGBR = 20, + WBGR = 21, +}; + +/** + * Get byte count per pixel for a color order + */ +inline constexpr uint8_t getChannelCount(ColorOrder order) { + return (static_cast(order) >= 10) ? 4 : 3; +} + +/** + * Check if color order includes white channel + */ +inline constexpr bool hasWhiteChannel(ColorOrder order) { + return static_cast(order) >= 10; +} + +//============================================================================== +// WLED Pixel Format - uint32_t RGBW +//============================================================================== + +/** + * Extract color components from WLED's uint32_t format + * Format: 0xWWRRGGBB (W in high byte, B in low byte) + */ +inline uint8_t getR(uint32_t color) { return (color >> 16) & 0xFF; } +inline uint8_t getG(uint32_t color) { return (color >> 8) & 0xFF; } +inline uint8_t getB(uint32_t color) { return color & 0xFF; } +inline uint8_t getW(uint32_t color) { return (color >> 24) & 0xFF; } + +/** + * Create uint32_t color from components + */ +inline uint32_t makeColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { + return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; +} + +//============================================================================== +// CCT (Warm White / Cool White) Support +//============================================================================== + +/** + * CCT data for a single pixel + * Passed as separate buffer from main RGBW data + */ +struct CctPixel { + uint8_t ww; // Warm white + uint8_t cw; // Cool white +}; + +//============================================================================== +// Driver State +//============================================================================== + +enum class DriverState : uint8_t { + Idle = 0, + Sending = 1, + WaitingReset = 2 +}; + +//============================================================================== +// DMA Buffer Configuration +//============================================================================== + +constexpr size_t DEFAULT_DMA_BUFFER_SIZE = 2048; +constexpr size_t MIN_DMA_BUFFER_SIZE = 256; +constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; + +//============================================================================== +// Base Bus Interface +//============================================================================== + +class IBus { +public: + virtual ~IBus() = default; + + virtual bool begin() = 0; + virtual void end() = 0; + + /** + * Show pixels + * @param pixels RGBW pixel data (uint32_t per pixel, WLED format) + * @param numPixels Number of pixels + * @param cct Optional CCT data (2 bytes per pixel: WW, CW) + * @return true if transmission started successfully + */ + virtual bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) = 0; + + virtual bool canShow() const = 0; + virtual void waitComplete() = 0; + virtual const char* getType() const = 0; +}; + +//============================================================================== +// Forward Declarations +//============================================================================== + +class RmtBus; + +#ifdef WLEDPB_I2S_SUPPORT +class I2sBus; +class I2sBusContext; +#endif + +#ifdef WLEDPB_LCD_SUPPORT +class LcdBus; +class LcdBusContext; +#endif + +//============================================================================== +// Color Encoding Helpers +//============================================================================== + +/** + * Encode pixel to byte stream according to color order + */ +class ColorEncoder { +public: + ColorEncoder(ColorOrder order) : _order(order), _numChannels(getChannelCount(order)) {} + + /** + * Encode a single pixel to output buffer + * @param pixel RGBW color value + * @param cct Optional CCT data (for CCT strips, replaces W channel) + * @param out Output buffer (must have space for numChannels bytes) + */ + void encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const; + + uint8_t getNumChannels() const { return _numChannels; } + ColorOrder getOrder() const { return _order; } + +private: + ColorOrder _order; + uint8_t _numChannels; +}; + +//============================================================================== +// RMT Bus - Works on all ESP32 variants +//============================================================================== + +#include "driver/rmt.h" + +class RmtBus : public IBus { +public: + /** + * Create RMT bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @param channel RMT channel (-1 for auto) + */ + RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, + int8_t channel = -1); + ~RmtBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "RMT"; } + + // Configuration + void setInverted(bool inv) { _inverted = inv; } + void setTiming(const LedTiming& timing); + void setColorOrder(ColorOrder order); + +private: + int8_t _pin; + int8_t _channel; + LedTiming _timing; + ColorOrder _order; + bool _inverted; + bool _initialized; + + rmt_channel_t _rmtChannel; + uint32_t _rmtBit0; + uint32_t _rmtBit1; + uint16_t _rmtResetTicks; + + // Encode buffer + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + + void updateRmtTiming(); + bool allocateBuffer(uint16_t numPixels); + + // Static translate callback + static void IRAM_ATTR translateCB(const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num); +}; + +//============================================================================== +// I2S Parallel Bus - ESP32 and ESP32-S2 +//============================================================================== + +#ifdef WLEDPB_I2S_SUPPORT + +#include "soc/i2s_struct.h" +#include "soc/i2s_reg.h" +#include "driver/periph_ctrl.h" +#include "rom/lldesc.h" +#include "esp_intr_alloc.h" + +#if defined(WLEDPB_ESP32) + #define WLEDPB_I2S_BUS_COUNT 2 + #define WLEDPB_I2S_MAX_CHANNELS 16 +#else + #define WLEDPB_I2S_BUS_COUNT 1 + #define WLEDPB_I2S_MAX_CHANNELS 8 +#endif + +/** + * I2S bus context - manages shared I2S peripheral for parallel output + * Uses double-buffered DMA with ISR-driven buffer refill + */ +class I2sBusContext { +public: + static I2sBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); + + bool init(const LedTiming& timing, size_t bufferSize); + void deinit(); + + // Channel management + int8_t registerChannel(int8_t pin, I2sBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + // Transmission + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } + + // Data access for channels + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + I2sBusContext(uint8_t busNum); + ~I2sBusContext(); + + void fillBuffer(uint8_t bufIdx); + static void IRAM_ATTR dmaISR(void* arg); + + uint8_t _busNum; + i2s_dev_t* _i2sDev; + volatile DriverState _state; + bool _initialized; + + // DMA double buffer + lldesc_t* _dmaDesc[2]; + uint8_t* _dmaBuffer[2]; + size_t _bufferSize; + volatile uint8_t _activeBuffer; + + // Timing + LedTiming _timing; + uint32_t _clockDiv; + + // ISR handle + intr_handle_t _isrHandle; + + // Channel data + struct ChannelData { + I2sBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; + uint8_t _channelCount; + uint16_t _channelMask; + size_t _maxDataLen; + + // Encoding (4-step cadence) + void encode4Step(uint8_t* dest, size_t destLen, size_t* bytesWritten); + + // Singleton instances + static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; +}; + +/** + * I2S parallel output bus + */ +class I2sBus : public IBus { +public: + /** + * Create I2S bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) + * @param bufferSize DMA buffer size + */ + I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, + uint8_t busNum = 0, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + ~I2sBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "I2S"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); + + // Access for context + const uint8_t* getEncodedData() const { return _encodeBuffer; } + size_t getEncodedLen() const { return _encodedLen; } + +private: + int8_t _pin; + uint8_t _busNum; + size_t _bufferSize; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + int8_t _channelIdx; + I2sBusContext* _ctx; + + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + size_t _encodedLen; + + bool allocateBuffer(uint16_t numPixels); +}; + +#endif // WLEDPB_I2S_SUPPORT + +//============================================================================== +// LCD Parallel Bus - ESP32-S3 only +//============================================================================== +#ifdef WLEDPB_LCD_SUPPORT + +#include "driver/periph_ctrl.h" +#include "esp_private/gdma.h" +#include "esp_rom_gpio.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" + +#ifndef WLEDPB_LCD_DMA_BUFFER_SIZE +#define WLEDPB_LCD_DMA_BUFFER_SIZE 2048 +#endif + +#ifndef WLEDPB_LCD_CADENCE_STEPS +#define WLEDPB_LCD_CADENCE_STEPS 4 +#endif + +#ifndef WLEDPB_LCD_DEBUG +#define WLEDPB_LCD_DEBUG 1 +#endif + +#define WLEDPB_LCD_MAX_CHANNELS 8 + +// Transmission state machine +enum class TxState : uint8_t { + Idle = 0, // Not transmitting + SendingData, // Sending pixel data + SendingReset, // Sending zeros for reset period + StopPending // Will stop on next EOF +}; + +class LcdBusContext { +public: + static LcdBusContext* get(); + static void release(); + + bool init(const LedTiming& timing, size_t bufferSize, bool use16Bit = false); + void deinit(); + + int8_t registerChannel(int8_t pin, LcdBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + bool startTransmit(); + bool isIdle() const { return _state == DriverState:: Idle; } + + void forceIdle() { + LCD_CAM.lcd_user.lcd_start = 0; + _state = DriverState::Idle; + _txState = TxState:: Idle; + } + + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + void printDebugStats(); + +private: + LcdBusContext(); + ~LcdBusContext(); + + size_t encodeIntoBuffer(uint8_t* dest, size_t destLen); + bool hasMoreData() const; + void fillBufferForState(uint8_t bufIdx); + + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data); + + volatile DriverState _state; + volatile TxState _txState; + bool _initialized; + bool _use16Bit; + + // DMA (circular linked list - never modified during operation) + gdma_channel_handle_t _dmaChannel; + dma_descriptor_t* _dmaDesc[2]; + uint8_t* _dmaBuffer[2]; + volatile uint8_t _activeBuffer; + + // Timing + LedTiming _timing; + size_t _resetBytesPerBuffer; + volatile size_t _resetBytesRemaining; + + // Channels + struct ChannelData { + LcdBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; + uint8_t _channelCount; + uint8_t _channelMask; + size_t _maxDataLen; + + static LcdBusContext* _instance; + static uint8_t _refCount; + + friend class LcdBus; +}; + +class LcdBus : public IBus { +public: + LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, + size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, bool use16Bit = false); + ~LcdBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "LCD"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); + +private: + bool allocateBuffer(uint16_t numPixels); + + int8_t _pin; + size_t _bufferSize; + bool _use16Bit; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + int8_t _channelIdx; + LcdBusContext* _ctx; + + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + size_t _encodedLen; +}; + +#endif // WLEDPB_LCD_SUPPORT + +//============================================================================== +// Bus Factory - Create appropriate bus for platform +//============================================================================== + +enum class BusType : uint8_t { + RMT = 0, + I2S = 1, + LCD = 2, + Auto = 255 +}; + +/** + * Create a bus instance + * @param type Bus type (Auto will select best for platform) + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @return Bus instance (caller owns, delete when done) + */ +IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, + ColorOrder order, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + +/** + * Get recommended bus type for current platform + */ +BusType getRecommendedBusType(); + +} // namespace WLEDpixelBus \ No newline at end of file From 74b5ac59b0b789e4f5fdc06ee4d07a2ab05613c7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 7 Mar 2026 11:23:30 +0100 Subject: [PATCH 003/173] basic RMT version (WS2812) working on C3 --- build.log | Bin 0 -> 7190 bytes .../include/NeoEsp32RmtHIMethod.h | 2 +- platformio.ini | 2 - wled00/bus_manager.cpp | 65 +- wled00/bus_manager.h | 3 +- wled00/bus_wrapper.h | 1121 ++--------------- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 97 +- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 117 +- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 4 + .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 109 ++ .../src/WLEDpixelBus/WLEDpixelBus_Features.h | 70 + wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 118 ++ wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h | 49 + .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 92 ++ 14 files changed, 724 insertions(+), 1125 deletions(-) create mode 100644 build.log create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h diff --git a/build.log b/build.log new file mode 100644 index 0000000000000000000000000000000000000000..fab16bc190f8e2d6f2387e0a07811533edddb60f GIT binary patch literal 7190 zcmd6sYfl?T6o%((EA>CDs49`BzTq^GRIN$?6I37+Nb{jY#MeN;m)ZtGRrRO0eV=nY z?D`UiP%2d`v%9l%&b;S-X88NBz0eQcP!G*8&~+L*;cZCltryB+Nngt0IP8WOp`p=Q zxUlbwp%>0WH4H;b)cw#7Pepa2_o2QF^j6UIR{VwQ9?(t5@4*!~tjVU2wFU6^~}J4w^h zdR5_UY6WCGGa0LTYDgk9)wOvwTbogI04~>D&UBa4cr#pz8eOzQE|kK9@J0AsPyg== zjK2tbU)JkNs-~pFYRHpFwyHFc=o@cMJy-RNbsBoVl(bdd-$=&G@Qt33^-^C`Zf`W& zm9B=G+Z8?QeQj4F{rY;%@LpdBHpe})9j@h%_)e^1P}yXHSGiANm%d0}h2gm6l9^=%Z*N15V0^c`uD3k{u$dy0gw z&NET;V8si4?`STOP_x-cc&d?cy~S0@5AbZT}xzY%F{86%cOcI?r@jRDKPjttR zvXoGz78?pzS96K@y7-WHs zvtyAD_i@hIFIftnAt)n(n!)R{4Z;UWRS|}5X@}g*-tD2G{!BZ5cXL?54J2v`AcSb6v)qy01|GLmU zlT|j2^O~|k-=yo9A1uleZApxksjOAWj#L|}6J*}LvIe$C`stZqajoZMBfP6&HDG+) z^AK6CV>lxPTJCB-v$6b82=G{tywe-j=i5825AB(~we9(IR5{6~O&celF-nCa6VgNF zj9X22Jg#x;c{%BhmFM$xiLS+c*6b)jPLhEB+Bgn{sBNC@J`Q2!-ke}=0)VH%TesOgwAEf)<&v_qgX6y z{*tK36+=;-gl`Smw*0KW+O)Wnv=7rp6tG<*FzyV z7X@`Q?U;zErlG)(bDGhR>}bewB#KP$mgwY(P-Km({i%^u1NhyVX1kZ*J87Q)erD5+ z5p8wdS*4=+vCk-nC&q7_1J{dWFt|OnFm-KmCaeZnA*fxcV z4C0yR4`~7~UKQSJ)AXSv+tUcXgh#kPAwfD96m7F69nPleNM%G(nn|&#`7N_57Ku7# z1?Ka^TcY>ZBx_mRTNV}BmEF&d#i6YXl45i&DThajfQD?cBu{H7nzB49;*4#!70c@x z9$qc7Gq;qt@fqK>H7)k{#BFKR8OQj=&+9;h&wy5>?uXMXT}?1reu#4spu=o!$(>(?tzZPBc3D7 zI)ORT*S47DLc~VxIK1s$JX8ry_BrJ z%Sv~8%*W#K{3$(sT2P#i@7UPoCEAzJZJ;l9Lw*99T4hVC6MJr@g^^v@wa^Q3{Tja4 z<{jqJP8mVtyQF2+(-l>!>>TgB(_IoM z904fe1rA5BY(4MeDFh|eRQ6$fXMZ>wMM-@E-&cfW9INA)&U*LdbM%dV3h22n_UZ7a zTe}?fU-So_OUWnx^tShHfUYCW20h^ivRgJCekg+q6_yHy}s6h{DJQLJ~>m_T0;7I zbcRhm1Anx@{rW;8+jb>ES{IM?excEx{XH^mlIzLZ%}lL3RY^12hN4!fbJLPDm()2P5Z;!KaeC-j_P^)<#$;V^^v z=-#aFc<{NH8*YEcTMmSMO&9&sf#sA#;pPOIJ-Y8{$qXJZ#0uwJ$sJY$BfkytpxTiM zI~EzM je*8^!=6UP()`&CN8=~)+i^w>hb%;B5II~x~t4;m^!cs3g literal 0 HcmV?d00001 diff --git a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h index 02e066f741..072f6c112d 100644 --- a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h +++ b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h @@ -31,7 +31,7 @@ License along with NeoPixel. If not, see #if defined(ARDUINO_ARCH_ESP32) // Use the NeoEspRmtSpeed types from the driver-based implementation -#include +#include namespace NeoEsp32RmtHiMethodDriver { diff --git a/platformio.ini b/platformio.ini index 60dedd473b..ec50e72ca0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -162,7 +162,6 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2 marvinroger/AsyncMqttClient @ 0.9.0 # for I2C interface @@ -250,7 +249,6 @@ lib_deps_compat = ESP8266PWM fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 7d0d976381..c01a2efe17 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -130,16 +130,15 @@ BusDigital::BusDigital(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) , _skip(bc.skipAmount) //sacrificial pixels , _colorOrder(bc.colorOrder) +, _iType(bc.iType) // internal bus type determined by getI() , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) , _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } - _iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit() if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } - if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; _colorSum = 0; _pins[0] = bc.pins[0]; @@ -158,13 +157,18 @@ BusDigital::BusDigital(const BusConfig &bc) _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); + + // create bus via PolyBus wrapper which will return a WLEDpixelBus::IBus + _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder); _valid = (_busPtr != nullptr) && bc.count > 0; + // fix for wled#4759 - if (_valid) for (unsigned i = 0; i < _skip; i++) { - PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here) - } - else { + if (_valid) { + _busPtr->allocatePixelBuffer(lenToCreate + _skip, _hasCCT); + for (unsigned i = 0; i < _skip; i++) { + _busPtr->setPixelColor(i, 0); // set sacrificial pixels to black (color order does not matter here) + } + } else { 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"), @@ -231,7 +235,8 @@ void BusDigital::applyBriLimit(uint8_t newBri) { 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 + uint32_t c = 0; + c = _busPtr->getPixelColor(i); // 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) @@ -239,7 +244,15 @@ void BusDigital::applyBriLimit(uint8_t newBri) { 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 + + if (hasCCT()) { + WLEDpixelBus::CctPixel cp; + cp.ww = wwcw & 0xFF; + cp.cw = wwcw >> 8; + _busPtr->setPixelColor(i, c, &cp); + } else { + _busPtr->setPixelColor(i, c); + } } } @@ -249,20 +262,20 @@ 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) + _busPtr->show(); } bool BusDigital::canShow() const { if (!_valid) return true; - return PolyBus::canShow(_busPtr, _iType); + return _busPtr->canShow(); } //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); + if (canShow()) _busPtr->show(); } } @@ -297,15 +310,22 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { 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) + uint32_t cOld = _busPtr->getPixelColor(pix); + switch (pOld % 3) { // change only the single channel 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; } } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); + if (hasCCT()) { + WLEDpixelBus::CctPixel cp; + cp.ww = wwcw & 0xFF; + cp.cw = wwcw >> 8; + _busPtr->setPixelColor(pix, c, &cp); + } else { + _busPtr->setPixelColor(pix, c); + } } // returns lossly restored color from bus @@ -314,7 +334,8 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { 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); + uint32_t rawC = _busPtr->getPixelColor((_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix); + uint32_t c = restoreColorLossy(rawC, _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? @@ -339,7 +360,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->getNumPixels() * sizeof(uint32_t) : 0); // does not include common I2S DMA buffer } void BusDigital::setColorOrder(uint8_t colorOrder) { @@ -380,12 +401,16 @@ bool BusDigital::isI2S() { void BusDigital::begin() { if (!_valid) return; - PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz); + _busPtr->begin(); } void BusDigital::cleanup() { DEBUGBUS_PRINTLN(F("Digital Cleanup.")); - PolyBus::cleanup(_busPtr, _iType); + if (_busPtr) { + _busPtr->end(); + delete _busPtr; + _busPtr = nullptr; + } _iType = I_NONE; _valid = false; _busPtr = nullptr; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index d5ac31a670..0a55cf42f9 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -13,6 +13,7 @@ * Class for addressing various light types */ +#include "src/WLEDpixelBus/WLEDpixelBus.h" #include "const.h" #include "pin_manager.h" #include @@ -283,7 +284,7 @@ class BusDigital : public Bus { uint8_t _milliAmpsPerLed; uint16_t _milliAmpsLimit; uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current - void *_busPtr; + WLEDpixelBus::IBus* _busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 0ecd4f986d..2e1ffcfc57 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -3,7 +3,7 @@ #define BusWrapper_h //#define NPB_CONF_4STEP_CADENCE -#include "NeoPixelBus.h" +#include "src/WLEDpixelBus/WLEDpixelBus.h" //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -134,209 +134,9 @@ #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 GRBCW -#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 GRBCW -#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 (RGBWC) -#define B_8266_U0_SM16825_5 NeoPixelBus -#define B_8266_U1_SM16825_5 NeoPixelBus -#define B_8266_DM_SM16825_5 NeoPixelBus -#define B_8266_BB_SM16825_5 NeoPixelBus -#endif - -/*** ESP32 Neopixel methods ***/ -#ifdef ARDUINO_ARCH_ESP32 -// C3: I2S0 and I2S1 methods not supported (has one I2S bus) -// S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus) -// S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (has two I2S buses) -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 -#if defined(CONFIG_IDF_TARGET_ESP32S3) - // S3 will always use LCD parallel output - typedef X8Ws2812xMethod X1Ws2812xMethod; - typedef X8Sk6812Method X1Sk6812Method; - typedef X8400KbpsMethod X1400KbpsMethod; - typedef X8800KbpsMethod X1800KbpsMethod; - typedef X8Tm1814Method X1Tm1814Method; - typedef X8Tm1829Method X1Tm1829Method; - typedef X8Apa106Method X1Apa106Method; - typedef X8Ws2805Method X1Ws2805Method; - typedef X8Tm1914Method X1Tm1914Method; -#elif defined(CONFIG_IDF_TARGET_ESP32S2) - // S2 will use I2S0 - typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod; - typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method; - typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod; - typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod; - typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method; - typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method; - typedef NeoEsp32I2s0Apa106Method X1Apa106Method; - typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method; - typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method; -#elif !defined(CONFIG_IDF_TARGET_ESP32C3) - // regular ESP32 will use I2S1 - typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod; - typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method; - typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod; - typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod; - typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method; - typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method; - typedef NeoEsp32I2s1Apa106Method X1Apa106Method; - typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method; - typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method; -#endif - -// RMT driver selection -#if !defined(WLED_USE_SHARED_RMT) && !defined(__riscv) -#include -#define NeoEsp32RmtMethod(x) NeoEsp32RmtHIN ## x ## Method -#else -#define NeoEsp32RmtMethod(x) NeoEsp32RmtN ## x ## Method -#endif - -//RGB -#define B_32_RN_NEO_3 NeoPixelBus // ESP32, S2, S3, C3 -//#define B_32_IN_NEO_3 NeoPixelBus // ESP32 (dynamic I2S selection) -#define B_32_I2_NEO_3 NeoPixelBus // ESP32, S2, S3 (automatic I2S selection, see typedef above) -#define B_32_IP_NEO_3 NeoPixelBus // parallel I2S (ESP32, S2, S3) -//RGBW -#define B_32_RN_NEO_4 NeoPixelBus -#define B_32_I2_NEO_4 NeoPixelBus -#define B_32_IP_NEO_4 NeoPixelBus // parallel I2S -//400Kbps -#define B_32_RN_400_3 NeoPixelBus -#define B_32_I2_400_3 NeoPixelBus -#define B_32_IP_400_3 NeoPixelBus // parallel I2S -//TM1814 (RGBW) -#define B_32_RN_TM1_4 NeoPixelBus -#define B_32_I2_TM1_4 NeoPixelBus -#define B_32_IP_TM1_4 NeoPixelBus // parallel I2S -//TM1829 (RGB) -#define B_32_RN_TM2_3 NeoPixelBus -#define B_32_I2_TM2_3 NeoPixelBus -#define B_32_IP_TM2_3 NeoPixelBus // parallel I2S -//UCS8903 -#define B_32_RN_UCS_3 NeoPixelBus -#define B_32_I2_UCS_3 NeoPixelBus -#define B_32_IP_UCS_3 NeoPixelBus // parallel I2S -//UCS8904 -#define B_32_RN_UCS_4 NeoPixelBus -#define B_32_I2_UCS_4 NeoPixelBus -#define B_32_IP_UCS_4 NeoPixelBus// parallel I2S -//APA106 -#define B_32_RN_APA106_3 NeoPixelBus -#define B_32_I2_APA106_3 NeoPixelBus -#define B_32_IP_APA106_3 NeoPixelBus // parallel I2S -//FW1906 GRBCW -#define B_32_RN_FW6_5 NeoPixelBus -#define B_32_I2_FW6_5 NeoPixelBus -#define B_32_IP_FW6_5 NeoPixelBus // parallel I2S -//WS2805 RGBWC -#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 (RGBWC) -#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 @@ -353,862 +153,102 @@ class PolyBus { // 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 (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)); - } - - // 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)); - } - - template - static void beginTM1914(void* busPtr) { - T tm1914_strip = static_cast(busPtr); - tm1914_strip->Begin(); - tm1914_strip->SetPixelSettings(NeoTm1914Settings()); //NeoTm1914_Mode_DinFdinAutoSwitch, NeoTm1914_Mode_DinOnly, NeoTm1914_Mode_FdinOnly + + static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz) { + if (busPtr) static_cast(busPtr)->begin(); } - - static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz /* only used by DotStar */) { - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_400_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_TM1_4: beginTM1814(busPtr); break; - case I_8266_U1_TM1_4: beginTM1814(busPtr); break; - case I_8266_DM_TM1_4: beginTM1814(busPtr); break; - case I_8266_BB_TM1_4: beginTM1814(busPtr); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_WS1_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_HS_P98_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U0_TM1914_3: beginTM1914(busPtr); break; - case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; - case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; - case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->Begin(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_TM1_4: beginTM1814(busPtr); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; - case I_32_RN_TM1914_3: beginTM1914(busPtr); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; - // I2S1 bus or parellel buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - #endif - // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() - case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_LPO_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_WS1_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - case I_HS_P98_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; - #endif - case I_SS_DOT_3: (static_cast(busPtr))->Begin(); break; - case I_SS_LPD_3: (static_cast(busPtr))->Begin(); break; - case I_SS_LPO_3: (static_cast(busPtr))->Begin(); break; - case I_SS_WS1_3: (static_cast(busPtr))->Begin(); break; - case I_SS_P98_3: (static_cast(busPtr))->Begin(); break; - } - } - - static void* create(uint8_t busType, uint8_t* pins, uint16_t len) { - void* busPtr = nullptr; - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: busPtr = new B_8266_U0_NEO_3(len, pins[0]); break; - case I_8266_U1_NEO_3: busPtr = new B_8266_U1_NEO_3(len, pins[0]); break; - case I_8266_DM_NEO_3: busPtr = new B_8266_DM_NEO_3(len, pins[0]); break; - case I_8266_BB_NEO_3: busPtr = new B_8266_BB_NEO_3(len, pins[0]); break; - case I_8266_U0_NEO_4: busPtr = new B_8266_U0_NEO_4(len, pins[0]); break; - case I_8266_U1_NEO_4: busPtr = new B_8266_U1_NEO_4(len, pins[0]); break; - case I_8266_DM_NEO_4: busPtr = new B_8266_DM_NEO_4(len, pins[0]); break; - case I_8266_BB_NEO_4: busPtr = new B_8266_BB_NEO_4(len, pins[0]); break; - case I_8266_U0_400_3: busPtr = new B_8266_U0_400_3(len, pins[0]); break; - case I_8266_U1_400_3: busPtr = new B_8266_U1_400_3(len, pins[0]); break; - case I_8266_DM_400_3: busPtr = new B_8266_DM_400_3(len, pins[0]); break; - case I_8266_BB_400_3: busPtr = new B_8266_BB_400_3(len, pins[0]); break; - case I_8266_U0_TM1_4: busPtr = new B_8266_U0_TM1_4(len, pins[0]); break; - case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; - case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; - case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; - case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break; - case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break; - case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break; - case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break; - case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; - case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; - case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; - case I_8266_BB_UCS_3: busPtr = new B_8266_BB_UCS_3(len, pins[0]); break; - case I_8266_U0_UCS_4: busPtr = new B_8266_U0_UCS_4(len, pins[0]); break; - case I_8266_U1_UCS_4: busPtr = new B_8266_U1_UCS_4(len, pins[0]); break; - case I_8266_DM_UCS_4: busPtr = new B_8266_DM_UCS_4(len, pins[0]); break; - case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break; - case I_8266_U0_APA106_3: busPtr = new B_8266_U0_APA106_3(len, pins[0]); break; - case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break; - case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break; - case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break; - case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break; - case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break; - case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break; - case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break; - case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break; - case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; - case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; - case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; - case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break; - case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; - case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; - case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; - case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break; - case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break; - case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break; - case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break; - case I_32_I2_NEO_4: if (_useParallelI2S) busPtr = new B_32_IP_NEO_4(len, pins[0]); else busPtr = new B_32_I2_NEO_4(len, pins[0]); break; - case I_32_I2_400_3: if (_useParallelI2S) busPtr = new B_32_IP_400_3(len, pins[0]); else busPtr = new B_32_I2_400_3(len, pins[0]); break; - case I_32_I2_TM1_4: if (_useParallelI2S) busPtr = new B_32_IP_TM1_4(len, pins[0]); else busPtr = new B_32_I2_TM1_4(len, pins[0]); break; - case I_32_I2_TM2_3: if (_useParallelI2S) busPtr = new B_32_IP_TM2_3(len, pins[0]); else busPtr = new B_32_I2_TM2_3(len, pins[0]); break; - case I_32_I2_UCS_3: if (_useParallelI2S) busPtr = new B_32_IP_UCS_3(len, pins[0]); else busPtr = new B_32_I2_UCS_3(len, pins[0]); break; - case I_32_I2_UCS_4: if (_useParallelI2S) busPtr = new B_32_IP_UCS_4(len, pins[0]); else busPtr = new B_32_I2_UCS_4(len, pins[0]); break; - case I_32_I2_APA106_3: if (_useParallelI2S) busPtr = new B_32_IP_APA106_3(len, pins[0]); else busPtr = new B_32_I2_APA106_3(len, pins[0]); break; - case I_32_I2_FW6_5: if (_useParallelI2S) busPtr = new B_32_IP_FW6_5(len, pins[0]); else busPtr = new B_32_I2_FW6_5(len, pins[0]); break; - case I_32_I2_2805_5: if (_useParallelI2S) busPtr = new B_32_IP_2805_5(len, pins[0]); else busPtr = new B_32_I2_2805_5(len, pins[0]); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) busPtr = new B_32_IP_TM1914_3(len, pins[0]); else busPtr = new B_32_I2_TM1914_3(len, pins[0]); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) busPtr = new B_32_IP_SM16825_5(len, pins[0]); else busPtr = new B_32_I2_SM16825_5(len, pins[0]); break; - #endif - #endif - // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) - case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; - case I_SS_DOT_3: busPtr = new B_SS_DOT_3(len, pins[1], pins[0]); break; - case I_HS_LPD_3: busPtr = new B_HS_LPD_3(len, pins[1], pins[0]); break; - case I_SS_LPD_3: busPtr = new B_SS_LPD_3(len, pins[1], pins[0]); break; - case I_HS_LPO_3: busPtr = new B_HS_LPO_3(len, pins[1], pins[0]); break; - case I_SS_LPO_3: busPtr = new B_SS_LPO_3(len, pins[1], pins[0]); break; - case I_HS_WS1_3: busPtr = new B_HS_WS1_3(len, pins[1], pins[0]); break; - case I_SS_WS1_3: busPtr = new B_SS_WS1_3(len, pins[1], pins[0]); break; - case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break; - case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break; + static void* cleanup(void* busPtr, uint8_t busType) { + if (busPtr) { + delete static_cast(busPtr); } - - return busPtr; + return nullptr; } - - static void show(void* busPtr, uint8_t busType, bool consistent = true) { - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - #endif - #endif - case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_LPD_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_LPD_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_LPO_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_LPO_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_WS1_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_WS1_3: (static_cast(busPtr))->Show(consistent); break; - case I_HS_P98_3: (static_cast(busPtr))->Show(consistent); break; - case I_SS_P98_3: (static_cast(busPtr))->Show(consistent); break; - } + static void show(void* busPtr, uint8_t busType, bool isI2s = false) { + if (busPtr) static_cast(busPtr)->show(); } - static bool canShow(void* busPtr, uint8_t busType) { - switch (busType) { - case I_NONE: return true; - #ifdef ESP8266 - case I_8266_U0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_400_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - #endif - #endif - case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_LPD_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_LPD_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_LPO_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_LPO_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_WS1_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_WS1_3: return (static_cast(busPtr))->CanShow(); break; - case I_HS_P98_3: return (static_cast(busPtr))->CanShow(); break; - case I_SS_P98_3: return (static_cast(busPtr))->CanShow(); break; - } + if (busPtr) return static_cast(busPtr)->canShow(); return true; } - - [[gnu::hot]] static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { - uint8_t r = c >> 16; - uint8_t g = c >> 8; - uint8_t b = c >> 0; - uint8_t w = c >> 24; - RgbwColor col; - uint8_t cctWW = wwcw & 0xFF, cctCW = (wwcw>>8) & 0xFF; - - // reorder channels to selected order - switch (co & 0x0F) { - default: col.G = g; col.R = r; col.B = b; break; //0 = GRB, default - case 1: col.G = r; col.R = g; col.B = b; break; //1 = RGB, common for WS2811 - case 2: col.G = b; col.R = r; col.B = g; break; //2 = BRG - case 3: col.G = r; col.R = b; col.B = g; break; //3 = RBG - case 4: col.G = b; col.R = g; col.B = r; break; //4 = BGR - case 5: col.G = g; col.R = b; col.B = r; break; //5 = GBR - } - // 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 - } - - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_DM_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_8266_U0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_U1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_DM_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_8266_U0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_DM_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_BB_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - #endif - case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_HS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - } + static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint32_t cW) { + if (busPtr) static_cast(busPtr)->setPixelColor(pix, c); } - - [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { - RgbwColor col(0,0,0,0); - switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_BB_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_8266_U0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_U1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_DM_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_BB_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_8266_U0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - #endif - #endif - case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_HS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_SS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - } - - // upper nibble contains W swap information - uint8_t w = col.W; - switch (co >> 4) { - case 1: col.W = col.B; col.B = w; break; // swap W & B - case 2: col.W = col.G; col.G = w; break; // swap W & G - case 3: col.W = col.R; col.R = w; break; // swap W & R - } - 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 - } + static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { + if (busPtr) static_cast(busPtr)->setBrightness(b); + } + static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t cW) { + if (busPtr) return static_cast(busPtr)->getPixelColor(pix); return 0; } - static void cleanup(void* busPtr, uint8_t busType) { - if (busPtr == nullptr) return; - switch (busType) { - case I_NONE: break; +static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder) { + Serial.printf("[WPB] PolyBus::create busType=%u pin=%u len=%u\n", busType, pins[0], len); + if (busType == I_NONE) { Serial.println("[WPB] busType=I_NONE, returning null"); return nullptr; } + auto btype = WLEDpixelBus::BusType::Auto; + auto timing = WLEDpixelBus::Timing::WS2812; + uint8_t hwIndex = pins[0]; + #ifdef ESP8266 - case I_8266_U0_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_U1_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_DM_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_BB_NEO_3: delete (static_cast(busPtr)); break; - case I_8266_U0_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_U1_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_DM_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_BB_NEO_4: delete (static_cast(busPtr)); break; - case I_8266_U0_400_3: delete (static_cast(busPtr)); break; - case I_8266_U1_400_3: delete (static_cast(busPtr)); break; - case I_8266_DM_400_3: delete (static_cast(busPtr)); break; - case I_8266_BB_400_3: delete (static_cast(busPtr)); break; - case I_8266_U0_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_BB_UCS_3: delete (static_cast(busPtr)); break; - case I_8266_U0_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_U1_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_DM_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_BB_UCS_4: delete (static_cast(busPtr)); break; - case I_8266_U0_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_U1_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_DM_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_BB_APA106_3: delete (static_cast(busPtr)); break; - case I_8266_U0_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_U1_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_DM_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_BB_FW6_5: delete (static_cast(busPtr)); break; - case I_8266_U0_2805_5: delete (static_cast(busPtr)); break; - case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; - case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; - case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; - case I_8266_U0_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; - case I_8266_U0_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_U1_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_DM_SM16825_5: delete (static_cast(busPtr)); break; - case I_8266_BB_SM16825_5: delete (static_cast(busPtr)); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses - case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; - case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; - case I_32_RN_400_3: delete (static_cast(busPtr)); break; - case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; - case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; - case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; - case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; - case I_32_RN_2805_5: delete (static_cast(busPtr)); break; - case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; - case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; - // I2S1 bus or paralell buses - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - #endif - #endif - case I_HS_DOT_3: delete (static_cast(busPtr)); break; - case I_SS_DOT_3: delete (static_cast(busPtr)); break; - case I_HS_LPD_3: delete (static_cast(busPtr)); break; - case I_SS_LPD_3: delete (static_cast(busPtr)); break; - case I_HS_LPO_3: delete (static_cast(busPtr)); break; - case I_SS_LPO_3: delete (static_cast(busPtr)); break; - case I_HS_WS1_3: delete (static_cast(busPtr)); break; - case I_SS_WS1_3: delete (static_cast(busPtr)); break; - case I_HS_P98_3: delete (static_cast(busPtr)); break; - case I_SS_P98_3: delete (static_cast(busPtr)); break; - } - } - - 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 + // ESP8266: timing selection based on iType ranges + if (busType >= I_8266_U0_NEO_3 && busType <= I_8266_BB_NEO_3) timing = WLEDpixelBus::Timing::WS2812; + else if (busType >= I_8266_U0_400_3 && busType <= I_8266_BB_400_3) timing = WLEDpixelBus::Timing::Generic400Kbps; + else if (busType >= I_8266_U0_TM1_4 && busType <= I_8266_BB_TM1_4) timing = WLEDpixelBus::Timing::TM1814; + else if (busType >= I_8266_U0_TM2_3 && busType <= I_8266_BB_TM2_3) timing = WLEDpixelBus::Timing::TM1829; + else if (busType >= I_8266_U0_UCS_3 && busType <= I_8266_BB_UCS_3) timing = WLEDpixelBus::Timing::UCS8903; + else if (busType >= I_8266_U0_UCS_4 && busType <= I_8266_BB_UCS_4) timing = WLEDpixelBus::Timing::UCS8904; + else if (busType >= I_8266_U0_APA106_3 && busType <= I_8266_BB_APA106_3) timing = WLEDpixelBus::Timing::APA106; + else if (busType >= I_8266_U0_FW6_5 && busType <= I_8266_BB_FW6_5) timing = WLEDpixelBus::Timing::FW1906; + else if (busType >= I_8266_U0_2805_5 && busType <= I_8266_BB_2805_5) timing = WLEDpixelBus::Timing::WS2805; + else if (busType >= I_8266_U0_TM1914_3 && busType <= I_8266_BB_TM1914_3) timing = WLEDpixelBus::Timing::TM1914; + else if (busType >= I_8266_U0_SM16825_5 && busType <= I_8266_BB_SM16825_5) timing = WLEDpixelBus::Timing::SM16825; + // ESP8266: bus type selection (UART0, UART1, DMA, BitBang) + if (busType == I_8266_U0_NEO_3 || busType == I_8266_U0_400_3 || busType == I_8266_U0_TM1_4 || busType == I_8266_U0_NEO_4 || busType == I_8266_U0_TM2_3 || busType == I_8266_U0_UCS_3 || busType == I_8266_U0_UCS_4 || busType == I_8266_U0_APA106_3 || busType == I_8266_U0_FW6_5 || busType == I_8266_U0_2805_5 || busType == I_8266_U0_TM1914_3 || busType == I_8266_U0_SM16825_5) btype = WLEDpixelBus::BusType::UART; + else if (busType == I_8266_U1_NEO_3 || busType == I_8266_U1_400_3 || busType == I_8266_U1_TM1_4 || busType == I_8266_U1_NEO_4 || busType == I_8266_U1_TM2_3 || busType == I_8266_U1_UCS_3 || busType == I_8266_U1_UCS_4 || busType == I_8266_U1_APA106_3 || busType == I_8266_U1_FW6_5 || busType == I_8266_U1_2805_5 || busType == I_8266_U1_TM1914_3 || busType == I_8266_U1_SM16825_5) btype = WLEDpixelBus::BusType::UART; + else if (busType == I_8266_DM_NEO_3 || busType == I_8266_DM_400_3 || busType == I_8266_DM_TM1_4 || busType == I_8266_DM_NEO_4 || busType == I_8266_DM_TM2_3 || busType == I_8266_DM_UCS_3 || busType == I_8266_DM_UCS_4 || busType == I_8266_DM_APA106_3 || busType == I_8266_DM_FW6_5 || busType == I_8266_DM_2805_5 || busType == I_8266_DM_TM1914_3 || busType == I_8266_DM_SM16825_5) btype = WLEDpixelBus::BusType::DMA; + else btype = WLEDpixelBus::BusType::BitBang; + #else + // ESP32: timing selection based on iType (RN=RMT odd, I2=I2S even) switch (busType) { - case I_NONE: break; - #ifdef ESP8266 - case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_NEO_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_NEO_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_400_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_400_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM1_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM2_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_UCS_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_UCS_4: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_APA106_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_FW6_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_2805_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - case I_8266_DM_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*5; break; - case I_8266_BB_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; - #endif - #ifdef ARDUINO_ARCH_ESP32 - // RMT buses (front + back + small system managed RMT) - case I_32_RN_NEO_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_NEO_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_400_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM1_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM2_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_UCS_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_UCS_4: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_APA106_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_FW6_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_2805_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_TM1914_3: size += (static_cast(busPtr))->PixelsSize()*2; break; - case I_32_RN_SM16825_5: size += (static_cast(busPtr))->PixelsSize()*2; break; - // I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; - #endif + case I_32_RN_NEO_3: case I_32_I2_NEO_3: timing = WLEDpixelBus::Timing::WS2812; break; + case I_32_RN_NEO_4: case I_32_I2_NEO_4: timing = WLEDpixelBus::Timing::WS2812; break; + case I_32_RN_400_3: case I_32_I2_400_3: timing = WLEDpixelBus::Timing::Generic400Kbps; break; + case I_32_RN_TM1_4: case I_32_I2_TM1_4: timing = WLEDpixelBus::Timing::TM1814; break; + case I_32_RN_TM2_3: case I_32_I2_TM2_3: timing = WLEDpixelBus::Timing::TM1829; break; + case I_32_RN_UCS_3: case I_32_I2_UCS_3: timing = WLEDpixelBus::Timing::UCS8903; break; + case I_32_RN_UCS_4: case I_32_I2_UCS_4: timing = WLEDpixelBus::Timing::UCS8904; break; + case I_32_RN_FW6_5: case I_32_I2_FW6_5: timing = WLEDpixelBus::Timing::FW1906; break; + case I_32_RN_APA106_3: case I_32_I2_APA106_3: timing = WLEDpixelBus::Timing::APA106; break; + case I_32_RN_2805_5: case I_32_I2_2805_5: timing = WLEDpixelBus::Timing::WS2805; break; + case I_32_RN_TM1914_3: case I_32_I2_TM1914_3: timing = WLEDpixelBus::Timing::TM1914; break; + case I_32_RN_SM16825_5: case I_32_I2_SM16825_5: timing = WLEDpixelBus::Timing::SM16825; break; + default: timing = WLEDpixelBus::Timing::WS2812; break; + } + // ESP32: bus type selection (odd iType = RMT, even = I2S/Auto) + if (busType & 0x01) btype = WLEDpixelBus::BusType::RMT; + // else: leave as Auto (will resolve to I2S or LCD depending on platform) #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; + + // Assign RMT channel from PolyBus tracking (incremented for each RMT bus) + int8_t rmtCh = -1; // default: let WLEDpixelBus auto-allocate + #ifndef ESP8266 + if (btype == WLEDpixelBus::BusType::RMT) { + if (_rmtChannel < WLED_MAX_RMT_CHANNELS) { + rmtCh = _rmtChannel++; + } else { + Serial.printf("[WPB] ERROR: no RMT channels left (max=%u)\n", WLED_MAX_RMT_CHANNELS); + return nullptr; + } } - return size; + #endif + + Serial.printf("[WPB] resolved: btype=%u timing_t0h=%u pin=%u rmtCh=%d\n", (unsigned)btype, timing.t0h_ns, hwIndex, rmtCh); + return WLEDpixelBus::createBus(btype, hwIndex, timing, colorOrder, len, rmtCh); } + [[gnu::hot]] + + [[gnu::hot]] + 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) { @@ -1287,6 +327,7 @@ class PolyBus { _i2sChannelsAssigned = 0; _parallelBusItype = I_NONE; _2PchannelsAssigned = 0; + WLEDpixelBus::RmtBus::resetAutoChannel(); // also reset WLEDpixelBus internal counter } #endif // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index d0f4fed1ae..5308fb208b 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -63,12 +63,16 @@ void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) con // RMT Bus Implementation //============================================================================== -// Thread-local context for RMT translate callback -static struct { +// Context for RMT translate callback - must be in DRAM for IRAM ISR access +static DRAM_ATTR struct { uint32_t bit0; uint32_t bit1; + uint16_t resetDuration; } s_rmtCtx; +// Static auto-channel counter for RmtBus +uint8_t RmtBus::s_nextAutoChannel = 0; + RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) : _pin(pin) , _channel(channel) @@ -128,14 +132,17 @@ bool RmtBus::begin() { // Auto-select channel if needed if (_channel < 0) { - _channel = 0; + // Use static counter for auto-allocation (fallback if caller didn't specify) + if (s_nextAutoChannel >= getRmtMaxChannels()) { + return false; + } + _channel = s_nextAutoChannel++; } -#if defined(WLEDPB_ESP32) - if (_channel > 7) return false; -#else - if (_channel > 3) return false; -#endif + if (_channel >= (int8_t)getRmtMaxChannels()) { + Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, getRmtMaxChannels()); + return false; + } _rmtChannel = (rmt_channel_t)_channel; updateRmtTiming(); @@ -158,20 +165,21 @@ bool RmtBus::begin() { esp_err_t err = rmt_config(&config); if (err != ESP_OK) { - log_e("RMT config failed: %d", err); return false; } - - err = rmt_driver_install(_rmtChannel, 0, ESP_INTR_FLAG_IRAM); + // Use interrupt flags matching NeoPixelBus behavior +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0) + err = rmt_driver_install(_rmtChannel, 0, ESP_INTR_FLAG_LOWMED); +#else + err = rmt_driver_install(_rmtChannel, 0, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1); +#endif if (err != ESP_OK) { - log_e("RMT driver install failed: %d", err); return false; } err = rmt_translator_init(_rmtChannel, translateCB); if (err != ESP_OK) { - log_e("RMT translator init failed: %d", err); rmt_driver_uninstall(_rmtChannel); return false; } @@ -214,7 +222,18 @@ bool RmtBus::allocateBuffer(uint16_t numPixels) { } bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!_initialized || !pixels || numPixels == 0) return false; + static uint32_t showDbgCnt = 0; + if (!pixels) { + pixels = _pixelData; + numPixels = _numPixels; + cct = _cctData; + } + if (numPixels == 0) numPixels = _numPixels; + if (!cct) cct = _cctData; + + if (!_initialized || !pixels || numPixels == 0) { + return false; + } // Wait for previous transmission rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); @@ -234,20 +253,21 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc // Update context for ISR s_rmtCtx.bit0 = _rmtBit0; s_rmtCtx.bit1 = _rmtBit1; + s_rmtCtx.resetDuration = _rmtResetTicks; // Start transmission size_t dataLen = numPixels * numCh; - esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, true); + if (showDbgCnt++ < 20) Serial.printf("[WPB] show() ch=%d numPix=%u numCh=%u dataLen=%u pix[0]=0x%08X\n", _rmtChannel, numPixels, numCh, dataLen, pixels[0]); + esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); + if (showDbgCnt <= 3 && err != ESP_OK) Serial.printf("[WPB] rmt_write_sample FAIL: %d (%s)\n", err, esp_err_to_name(err)); return err == ESP_OK; } bool RmtBus::canShow() const { if (!_initialized) return true; - - rmt_channel_status_result_t status; - rmt_get_channel_status(&status); - return status.status[_rmtChannel] == RMT_CHANNEL_IDLE; + // Use rmt_wait_tx_done with 0 timeout to check if TX is done (matching NeoPixelBus) + return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); } void RmtBus::waitComplete() { @@ -283,16 +303,37 @@ void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, uint32_t bit0 = s_rmtCtx.bit0; uint32_t bit1 = s_rmtCtx.bit1; + uint16_t resetDuration = s_rmtCtx.resetDuration; - while (size < src_size && num < wanted_num) { - uint8_t byte = *psrc++; - size++; + // Each byte produces 8 RMT items + for (;;) + { + uint8_t data = *psrc; - for (int i = 7; i >= 0 && num < wanted_num; i--) { - pdest->val = (byte & (1 << i)) ? bit1 : bit0; + for (uint8_t bit = 0; bit < 8; bit++) + { + pdest->val = (data & 0x80) ? bit1 : bit0; pdest++; - num++; + data <<= 1; + } + num += 8; + size++; + + // If this is the last byte, extend the last bit's LOW duration + // to include the full reset signal length + if (size >= src_size) + { + pdest--; + pdest->duration1 = resetDuration; + break; + } + + if (num >= wanted_num) + { + break; } + + psrc++; } *translated_size = size; @@ -1591,17 +1632,19 @@ void LcdBus::setColorOrder(ColorOrder order) { //============================================================================== IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, - ColorOrder order, size_t bufferSize) { + ColorOrder order, size_t bufferSize, int8_t channel) { + Serial.printf("[WPB] createBus type=%u pin=%d bufSize=%u ch=%d\n", (unsigned)type, pin, bufferSize, channel); if (type == BusType::Auto) { type = getRecommendedBusType(); + Serial.printf("[WPB] Auto resolved to type=%u\n", (unsigned)type); } IBus* bus = nullptr; switch (type) { case BusType::RMT: - bus = new RmtBus(pin, timing, order); + bus = new RmtBus(pin, timing, order, channel); break; #ifdef WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 4aec0915ac..2510f8ce0f 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -29,6 +29,7 @@ by @dedehai, 2026 #include "esp_attr.h" #include "driver/gpio.h" #include "esp_idf_version.h" +#include "driver/rmt.h" // Platform detection #if defined(CONFIG_IDF_TARGET_ESP32) @@ -51,6 +52,8 @@ by @dedehai, 2026 #define WLEDPB_LCD_SUPPORT #endif +#include "WLEDpixelBus_Timings.h" + namespace WLEDpixelBus { //============================================================================== @@ -60,38 +63,7 @@ namespace WLEDpixelBus { /** * LED timing parameters in nanoseconds */ -struct LedTiming { - uint16_t t0h_ns; // '0' bit high time - uint16_t t0l_ns; // '0' bit low time - uint16_t t1h_ns; // '1' bit high time - uint16_t t1l_ns; // '1' bit low time - uint32_t reset_us; // Reset/latch time in microseconds - - constexpr LedTiming(uint16_t t0h, uint16_t t0l, uint16_t t1h, uint16_t t1l, uint32_t reset) - : t0h_ns(t0h), t0l_ns(t0l), t1h_ns(t1h), t1l_ns(t1l), reset_us(reset) {} - - // Calculate bit period in ns - constexpr uint32_t bitPeriod() const { - return (t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2; - } -}; -// Predefined timing constants -namespace Timing { - constexpr LedTiming WS2812 {200, 750, 500, 450, 100}; - constexpr LedTiming WS2811 {300, 950, 900, 350, 300}; - constexpr LedTiming WS2813 {400, 850, 800, 450, 300}; - constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; - constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; - constexpr LedTiming SK6812 {400, 850, 800, 450, 80}; - constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; - constexpr LedTiming TM1829 {300, 900, 800, 400, 200}; - constexpr LedTiming APA106 {350, 1350, 1350, 350, 50}; - constexpr LedTiming UCS1903 {400, 850, 800, 450, 500}; - constexpr LedTiming SM16703 {300, 900, 900, 300, 80}; - constexpr LedTiming Generic800Kbps {400, 850, 800, 450, 300}; - constexpr LedTiming Generic400Kbps {800, 1700, 1600, 900, 300}; -} //============================================================================== // Color Order Configuration @@ -190,8 +162,17 @@ constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; //============================================================================== class IBus { +protected: + uint32_t* _pixelData = nullptr; + CctPixel* _cctData = nullptr; + uint16_t _numPixels = 0; + uint8_t _brightness = 255; + public: - virtual ~IBus() = default; + virtual ~IBus() { + if (_pixelData) free(_pixelData); + if (_cctData) free(_cctData); + } virtual bool begin() = 0; virtual void end() = 0; @@ -203,12 +184,57 @@ class IBus { * @param cct Optional CCT data (2 bytes per pixel: WW, CW) * @return true if transmission started successfully */ - virtual bool show(const uint32_t* pixels, uint16_t numPixels, + virtual bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) = 0; virtual bool canShow() const = 0; virtual void waitComplete() = 0; virtual const char* getType() const = 0; + + virtual bool allocatePixelBuffer(uint16_t numPixels, bool hasCCT = false) { + if (_pixelData) { + if (_numPixels == numPixels) return true; + free(_pixelData); + _pixelData = nullptr; + } + if (_cctData) { + free(_cctData); + _cctData = nullptr; + } + + _numPixels = numPixels; + if (numPixels == 0) return true; + + _pixelData = (uint32_t*)malloc(numPixels * sizeof(uint32_t)); + if (!_pixelData) return false; + memset(_pixelData, 0, numPixels * sizeof(uint32_t)); + + if (hasCCT) { + _cctData = (CctPixel*)malloc(numPixels * sizeof(CctPixel)); + if (_cctData) memset(_cctData, 0, numPixels * sizeof(CctPixel)); + } + + return true; + } + + virtual void setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) { + if (pix >= _numPixels || !_pixelData) return; + _pixelData[pix] = c; + if (cp && _cctData) _cctData[pix] = *cp; + } + + virtual uint32_t getPixelColor(uint16_t pix) const { + if (pix >= _numPixels || !_pixelData) return 0; + return _pixelData[pix]; + } + + virtual uint16_t getNumPixels() const { + return _numPixels; + } + + virtual void setBrightness(uint8_t b) { + _brightness = b; + } }; //============================================================================== @@ -287,6 +313,9 @@ class RmtBus : public IBus { void setTiming(const LedTiming& timing); void setColorOrder(ColorOrder order); + // Reset the auto-allocation counter (call before re-creating buses) + static void resetAutoChannel() { s_nextAutoChannel = 0; } + private: int8_t _pin; int8_t _channel; @@ -304,6 +333,8 @@ class RmtBus : public IBus { uint8_t* _encodeBuffer; size_t _encodeBufferSize; + static uint8_t s_nextAutoChannel; // auto-allocation counter + void updateRmtTiming(); bool allocateBuffer(uint16_t numPixels); @@ -614,16 +645,34 @@ enum class BusType : uint8_t { Auto = 255 }; +/** + * Get the maximum number of RMT TX channels for the current platform + */ +constexpr uint8_t getRmtMaxChannels() { +#if defined(WLEDPB_ESP32) + return 8; // ESP32 original: 8 RMT channels +#elif defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) + return 4; // ESP32-S2/S3: 4 RMT TX channels +#elif defined(WLEDPB_ESP32C3) + return 2; // ESP32-C3: 2 RMT TX channels +#else + return 0; +#endif +} + /** * Create a bus instance * @param type Bus type (Auto will select best for platform) * @param pin GPIO pin * @param timing LED timing * @param order Color order + * @param bufferSize DMA buffer size (for I2S/LCD) + * @param channel RMT channel to use (-1 for auto-allocate) * @return Bus instance (caller owns, delete when done) */ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, - ColorOrder order, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + ColorOrder order, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, + int8_t channel = -1); /** * Get recommended bus type for current platform diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp new file mode 100644 index 0000000000..946a326d29 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -0,0 +1,4 @@ +#include "WLEDpixelBus.h" +#ifdef WLEDPB_ESP8266 + +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h new file mode 100644 index 0000000000..1d22a7176e --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -0,0 +1,109 @@ +#pragma once + +#include "WLEDpixelBus.h" + +#ifdef WLEDPB_ESP8266 + +namespace WLEDpixelBus { + +//============================================================================== +// ESP8266 UART Bus (Asynchronous via UART1/UART0) +//============================================================================== + +class Esp8266UartBus : public IBus { +public: + Esp8266UartBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~Esp8266UartBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "ESP8266_UART"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } + +private: + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + uint8_t* _encodeLut; + + void updateUartTiming(); + bool allocateBuffer(size_t encodedDataLen); + void buildLut(); + + uint8_t* _encodeBuffer = nullptr; + size_t _encodeBufferSize = 0; +}; + +//============================================================================== +// ESP8266 DMA Bus (Via I2S) +//============================================================================== + +class Esp8266DmaBus : public IBus { +public: + Esp8266DmaBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~Esp8266DmaBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "ESP8266_DMA"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } + +private: + int8_t _pin; // Only RX pin (GPIO3) supported for I2S DMA on ESP8266 + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + void updateI2sTiming(); + bool allocateBuffer(size_t numPixels); + + uint8_t* _encodeBuffer = nullptr; + size_t _encodeBufferSize = 0; +}; + +//============================================================================== +// ESP8266 BitBang Bus +//============================================================================== + +class Esp8266BitBangBus : public IBus { +public: + Esp8266BitBangBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~Esp8266BitBangBus() override; + + bool begin() override; + void end() override; + + bool show() override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "ESP8266_BB"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } + +private: + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + // Cycle counts + uint32_t _t0h, _t0l, _t1h, _t1l; +}; + +} // namespace WLEDpixelBus + +#endif // WLEDPB_ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h new file mode 100644 index 0000000000..7f52132292 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + + +namespace WLEDpixelBus { + +/** + * Handle chip-specific prefix padding before the pixel data starts. + * For example: + * TM1814 requires a global 4-byte current configuration before the pixel data. + * TM1914 requires a 14-byte configuration setting before the pixel data. + */ +class FeaturePadder { +public: + FeaturePadder() : _prefixLen(0) {} + + /** + * Set up the prefix for TM1814 + * Format is: 4 bytes of config + 4 bytes of inverted config + * Data: R_mA, G_mA, B_mA, W_mA (typically 225 = 0xE1 each for 22.5mA) + */ + void setupTM1814(uint8_t r_mA = 225, uint8_t g_mA = 225, uint8_t b_mA = 225, uint8_t w_mA = 225) { + _prefix[0] = r_mA; + _prefix[1] = g_mA; + _prefix[2] = b_mA; + _prefix[3] = w_mA; + _prefix[4] = ~r_mA; + _prefix[5] = ~g_mA; + _prefix[6] = ~b_mA; + _prefix[7] = ~w_mA; + _prefixLen = 8; + } + + /** + * Set up the prefix for TM1914 + * Mode settings configuration. + * Mode 1: DIN/FDIN auto-switch (Default WLED setup) + * Format is typically 14 bytes (7 normal, 7 inverted) + */ + void setupTM1914() { + // Mode: DinFdinAutoSwitch + uint8_t modeData = 0x01; + + for (int i = 0; i < 7; i++) { + _prefix[i] = modeData; + _prefix[i + 7] = ~modeData; + } + _prefixLen = 14; + } + + bool hasPrefix() const { + return _prefixLen > 0; + } + + uint8_t getPrefixLen() const { + return _prefixLen; + } + + const uint8_t* getPrefixData() const { + return _prefix; + } + +private: + uint8_t _prefix[16]; + uint8_t _prefixLen; +}; + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp new file mode 100644 index 0000000000..ef09439238 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -0,0 +1,118 @@ +#include "WLEDpixelBus_SPI.h" + +namespace WLEDpixelBus { + +SpiBus::SpiBus(int8_t dataPin, int8_t clockPin, const LedTiming& timing, ColorOrder order, bool useHardwareSpi) + : _dataPin(dataPin), _clockPin(clockPin), _timing(timing), _order(order), + _useHardware(useHardwareSpi), _initialized(false) { +} + +SpiBus::~SpiBus() { + end(); +} + +bool SpiBus::begin() { + if (_initialized) return true; + + if (_useHardware) { + SPI.begin(); + // Assuming around 2MHz for now - in reality this would use timing parameters + SPI.setFrequency(2000000); + SPI.setDataMode(SPI_MODE0); + } else { + pinMode(_dataPin, OUTPUT); + pinMode(_clockPin, OUTPUT); + digitalWrite(_clockPin, LOW); + digitalWrite(_dataPin, LOW); + } + + _initialized = true; + return true; +} + +void SpiBus::end() { + if (!_initialized) return; + if (_useHardware) { + SPI.end(); + } else { + pinMode(_dataPin, INPUT); + pinMode(_clockPin, INPUT); + } + _initialized = false; +} + +void SpiBus::sendByte(uint8_t d) { + if (_useHardware) { + SPI.transfer(d); + } else { + // Bitbang SPI + for (uint8_t i = 0; i < 8; i++) { + if (d & 0x80) { + digitalWrite(_dataPin, HIGH); + } else { + digitalWrite(_dataPin, LOW); + } + digitalWrite(_clockPin, HIGH); + d <<= 1; + digitalWrite(_clockPin, LOW); + } + } +} + +void SpiBus::sendStartFrame() { + for (int i = 0; i < 4; i++) { + sendByte(0x00); + } +} + +void SpiBus::sendEndFrame() { + // Basic end frame for APA102 + for (int i = 0; i < 4; i++) { + sendByte(0xFF); + } +} + +bool SpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!pixels) pixels = _pixelData; + if (numPixels == 0) numPixels = _numPixels; + if (!_initialized || !pixels || _numPixels == 0) return false; + + // Output generic APA102/DotStar format + sendStartFrame(); + + for (uint16_t i = 0; i < _numPixels; i++) { + uint32_t c = pixels[i]; + + // For APA102: Global brightness + RGB (0xFF is max brightness) + uint8_t r = getR(c); + uint8_t g = getG(c); + uint8_t b = getB(c); + + // Start byte contains 3 bits of 1 and 5 bits of global brightness (could be dynamic) + sendByte(0xFF); + + // Output according to ColorOrder + switch (_order) { + case ColorOrder::RGB: sendByte(r); sendByte(g); sendByte(b); break; + case ColorOrder::GRB: sendByte(g); sendByte(r); sendByte(b); break; + case ColorOrder::BRG: sendByte(b); sendByte(r); sendByte(g); break; + case ColorOrder::RBG: sendByte(r); sendByte(b); sendByte(g); break; + case ColorOrder::GBR: sendByte(g); sendByte(b); sendByte(r); break; + case ColorOrder::BGR: sendByte(b); sendByte(g); sendByte(r); break; + default: sendByte(b); sendByte(g); sendByte(r); break; // Default APA102 BGR + } + } + + sendEndFrame(); + + return true; +} + +bool SpiBus::canShow() const { + return true; +} + +void SpiBus::waitComplete() { +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h new file mode 100644 index 0000000000..c3b172ad5a --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -0,0 +1,49 @@ +#pragma once + +#include "WLEDpixelBus.h" +#include + +namespace WLEDpixelBus { + +//============================================================================== +// SPI Bus (Hardware and Software SPI for 2-wire LEDs like APA102, WS2801) +//============================================================================== + +class SpiBus : public IBus { +public: + /** + * Create SPI bus + * @param dataPin MOSI pin + * @param clockPin SCLK pin + * @param timing LED timing parameters (used mostly for bit rate/clock speed) + * @param order Color order + * @param useHardwareSpi True to use hardware SPI peripheral (faster), false for bit-bang + */ + SpiBus(int8_t dataPin, int8_t clockPin, const LedTiming& timing, ColorOrder order, bool useHardwareSpi = true); + ~SpiBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return _useHardware ? "HW_SPI" : "SW_SPI"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } + +private: + int8_t _dataPin; + int8_t _clockPin; + LedTiming _timing; + ColorOrder _order; + bool _useHardware; + bool _initialized; + + void sendByte(uint8_t d); + void sendStartFrame(); + void sendEndFrame(); +}; + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h new file mode 100644 index 0000000000..e1111515c1 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +namespace WLEDpixelBus { + +/** + * LED timing parameters in nanoseconds + */ +struct LedTiming { + uint16_t t0h_ns; // '0' bit high time + uint16_t t0l_ns; // '0' bit low time + uint16_t t1h_ns; // '1' bit high time + uint16_t t1l_ns; // '1' bit low time + uint32_t reset_us; // Reset/latch time in microseconds + + constexpr LedTiming(uint16_t t0h, uint16_t t0l, uint16_t t1h, uint16_t t1l, uint32_t reset) + : t0h_ns(t0h), t0l_ns(t0l), t1h_ns(t1h), t1l_ns(t1l), reset_us(reset) {} + + // Calculate bit period in ns + constexpr uint32_t bitPeriod() const { + return (t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2; + } +}; + +// Predefined timing constants +namespace Timing { + // ---- Standard 1-wire LEDs (800KHz family) ---- + constexpr LedTiming WS2812 {300, 900, 800, 450, 100}; // WS2812B + constexpr LedTiming WS2811 {300, 950, 900, 350, 300}; // WS2811 (12V) + constexpr LedTiming WS2813 {400, 850, 800, 450, 300}; // WS2813 (backup data) + constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; // WS2815 (12V, 255mA) + constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; // WS2805 RGBCW (5ch) + constexpr LedTiming SK6812 {400, 850, 800, 450, 80}; // SK6812 / SK6812 RGBW + + // ---- Titan Micro LEDs ---- + constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; // TM1814 RGBW, requires prefix + constexpr LedTiming TM1829 {300, 900, 800, 400, 200}; // TM1829 (inverted logic) + constexpr LedTiming TM1914 {360, 890, 720, 530, 200}; // TM1914 RGB, requires prefix + + // ---- Other 1-wire LEDs ---- + constexpr LedTiming APA106 {350, 1350, 1350, 350, 50}; // APA106 / PL9823 + constexpr LedTiming UCS8903 {400, 850, 800, 450, 500}; // UCS8903 RGB (16bit) + constexpr LedTiming UCS8904 {400, 850, 800, 450, 500}; // UCS8904 RGBW (16bit) + constexpr LedTiming SM16703 {300, 900, 900, 300, 80}; // SM16703 + constexpr LedTiming SM16825 {300, 900, 900, 300, 80}; // SM16825 RGBCW (16bit, 5ch) + constexpr LedTiming FW1906 {400, 850, 800, 450, 300}; // FW1906 GRBCW (5ch) + + // ---- 400 KHz LEDs ---- + constexpr LedTiming Generic800Kbps {400, 850, 800, 450, 300}; + constexpr LedTiming Generic400Kbps {800, 1700, 1600, 900, 300}; + + // ---- 2-wire (SPI) LEDs - timing represents SPI clock speed ---- + // For SPI LEDs the timing struct is repurposed: bitPeriod() gives the SPI clock period + // Default SPI clock ~2MHz (500ns period) + constexpr LedTiming APA102 {250, 250, 250, 250, 0}; // APA102 / DotStar (up to ~20MHz) + constexpr LedTiming WS2801 {500, 500, 500, 500, 1000}; // WS2801 (max ~2MHz, 500us latch) + constexpr LedTiming LPD8806 {250, 250, 250, 250, 0}; // LPD8806 (latch = 0-bits) + constexpr LedTiming LPD6803 {250, 250, 250, 250, 0}; // LPD6803 + constexpr LedTiming P9813 {250, 250, 250, 250, 0}; // P9813 (Total Control Lighting) +} + +// ---- I2S / LCD Clock Calculation Helpers ---- + +/** + * Calculate the I2S/LCD clock divider from LED timing and cadence steps + * @param timing LED timing parameters + * @param cadenceSteps Number of cadence steps (3 or 4) + * @param baseClockMHz Base clock in MHz (160 for ESP32, 80 for S2, 240 for S3 LCD) + * @return Clock divider value + */ +inline uint32_t calcClockDiv(const LedTiming& timing, uint8_t cadenceSteps, uint32_t baseClockMHz) { + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t stepPeriodNs = bitPeriodNs / cadenceSteps; + uint32_t div = (baseClockMHz * stepPeriodNs) / 1000; + return (div < 2) ? 2 : (div > 255) ? 255 : div; +} + +/** + * Calculate reset bytes needed for I2S/LCD DMA + * @param timing LED timing parameters + * @param cadenceSteps Number of cadence steps + * @return Number of zero-bytes needed for reset period + */ +inline uint32_t calcResetBytes(const LedTiming& timing, uint8_t cadenceSteps) { + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t clockPeriodNs = bitPeriodNs / cadenceSteps; + uint32_t bytesPerUs = (clockPeriodNs > 0) ? (1000 / clockPeriodNs) : 1; + return timing.reset_us * bytesPerUs; +} + +} // namespace WLEDpixelBus From bb2a3c5cd82b8969d9f7bac327a0c4b479329dee Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 8 Mar 2026 07:30:12 +0100 Subject: [PATCH 004/173] add preliminary rgbw support --- wled00/bus_wrapper.h | 53 ++++++++++++++++++- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 2e1ffcfc57..04f861dbb3 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -241,8 +241,59 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #endif + // Determine channel count to parse correct WLEDpixelBus::ColorOrder + uint8_t channels = 3; + switch (busType) { + case I_32_RN_NEO_4: case I_32_I2_NEO_4: + case I_32_RN_TM1_4: case I_32_I2_TM1_4: + case I_32_RN_UCS_4: case I_32_I2_UCS_4: + #ifdef ESP8266 + case I_8266_U0_NEO_4: case I_8266_U1_NEO_4: case I_8266_DM_NEO_4: case I_8266_BB_NEO_4: + case I_8266_U0_TM1_4: case I_8266_U1_TM1_4: case I_8266_DM_TM1_4: case I_8266_BB_TM1_4: + case I_8266_U0_UCS_4: case I_8266_U1_UCS_4: case I_8266_DM_UCS_4: case I_8266_BB_UCS_4: + #endif + channels = 4; + break; + case I_32_RN_FW6_5: case I_32_I2_FW6_5: + case I_32_RN_2805_5: case I_32_I2_2805_5: + case I_32_RN_SM16825_5: case I_32_I2_SM16825_5: + #ifdef ESP8266 + case I_8266_U0_FW6_5: case I_8266_U1_FW6_5: case I_8266_DM_FW6_5: case I_8266_BB_FW6_5: + case I_8266_U0_2805_5: case I_8266_U1_2805_5: case I_8266_DM_2805_5: case I_8266_BB_2805_5: + case I_8266_U0_SM16825_5: case I_8266_U1_SM16825_5: case I_8266_DM_SM16825_5: case I_8266_BB_SM16825_5: + #endif + channels = 5; + break; + } + + // Map WLED UI order (0-5) to WLEDpixelBus order (which natively handles byte extraction) + uint8_t wledOrder = (uint8_t)colorOrder & 0x0F; + WLEDpixelBus::ColorOrder finalOrder = WLEDpixelBus::ColorOrder::GRB; + + if (channels >= 4) { + switch (wledOrder) { + case 0: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; + case 1: finalOrder = WLEDpixelBus::ColorOrder::RGBW; break; + case 2: finalOrder = WLEDpixelBus::ColorOrder::BRGW; break; + case 3: finalOrder = WLEDpixelBus::ColorOrder::RBGW; break; + case 4: finalOrder = WLEDpixelBus::ColorOrder::BGRW; break; // Note WLED's 4 is BGR, so we explicitly select BGRW + case 5: finalOrder = WLEDpixelBus::ColorOrder::GBRW; break; // Note WLED's 5 is GBR, so we explicitly select GBRW + default: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; + } + } else { + switch (wledOrder) { + case 0: finalOrder = WLEDpixelBus::ColorOrder::GRB; break; + case 1: finalOrder = WLEDpixelBus::ColorOrder::RGB; break; + case 2: finalOrder = WLEDpixelBus::ColorOrder::BRG; break; + case 3: finalOrder = WLEDpixelBus::ColorOrder::RBG; break; + case 4: finalOrder = WLEDpixelBus::ColorOrder::BGR; break; // WLED 4 is BGR, WPB enum for BGR is 5 but explicit is safer + case 5: finalOrder = WLEDpixelBus::ColorOrder::GBR; break; // WLED 5 is GBR, WPB enum for GBR is 4 but explicit is safer + default: finalOrder = WLEDpixelBus::ColorOrder::GRB; break; + } + } + Serial.printf("[WPB] resolved: btype=%u timing_t0h=%u pin=%u rmtCh=%d\n", (unsigned)btype, timing.t0h_ns, hwIndex, rmtCh); - return WLEDpixelBus::createBus(btype, hwIndex, timing, colorOrder, len, rmtCh); + return WLEDpixelBus::createBus(btype, hwIndex, timing, finalOrder, len, rmtCh); } [[gnu::hot]] diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index e1111515c1..3f22770306 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -31,7 +31,7 @@ namespace Timing { constexpr LedTiming WS2813 {400, 850, 800, 450, 300}; // WS2813 (backup data) constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; // WS2815 (12V, 255mA) constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; // WS2805 RGBCW (5ch) - constexpr LedTiming SK6812 {400, 850, 800, 450, 80}; // SK6812 / SK6812 RGBW + constexpr LedTiming SK6812 {300, 900, 800, 450, 200}; // SK6812 / SK6812 RGBW // ---- Titan Micro LEDs ---- constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; // TM1814 RGBW, requires prefix From 77acf8a6f4c2b80bcd0524b7c4e7d18df6082e53 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 8 Mar 2026 10:05:16 +0100 Subject: [PATCH 005/173] fix rmt channels not awaiting finish TX of all channels --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 13 +++++++++++-- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 5308fb208b..84d8975619 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -72,6 +72,7 @@ static DRAM_ATTR struct { // Static auto-channel counter for RmtBus uint8_t RmtBus::s_nextAutoChannel = 0; +uint8_t RmtBus::s_activeChannelMask = 0; RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) : _pin(pin) @@ -185,12 +186,14 @@ bool RmtBus::begin() { } _initialized = true; + s_activeChannelMask |= (1 << _channel); return true; } void RmtBus::end() { if (!_initialized) return; + s_activeChannelMask &= ~(1 << _channel); rmt_driver_uninstall(_rmtChannel); if (_encodeBuffer) { @@ -235,8 +238,14 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc return false; } - // Wait for previous transmission - rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + // Wait for ALL active RMT channels to complete their previous transmission. + // This ensures frame-level synchronization when multiple RMT outputs are used + // (prevents one channel from starting a new frame while another is still sending). + for (uint8_t ch = 0; ch < getRmtMaxChannels(); ch++) { + if (s_activeChannelMask & (1 << ch)) { + rmt_wait_tx_done((rmt_channel_t)ch, portMAX_DELAY); + } + } if (!allocateBuffer(numPixels)) return false; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 2510f8ce0f..b662ab2532 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -334,6 +334,7 @@ class RmtBus : public IBus { size_t _encodeBufferSize; static uint8_t s_nextAutoChannel; // auto-allocation counter + static uint8_t s_activeChannelMask; // bitmask of initialized channels void updateRmtTiming(); bool allocateBuffer(uint16_t numPixels); From 4ea2ee5181ed39d13e16fc62c47b7d18bbebbe64 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 8 Mar 2026 12:08:21 +0100 Subject: [PATCH 006/173] fix bus type --- wled00/FX_fcn.cpp | 2 +- wled00/bus_manager.cpp | 10 +-- wled00/bus_manager.h | 2 +- wled00/bus_wrapper.h | 147 +++++++++++++++++++---------------------- 4 files changed, 76 insertions(+), 85 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e8f40fdf19..774a9fde12 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1204,7 +1204,7 @@ void WS2812FX::finalizeInit() { mem += busMemUsage; // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) - bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT + bool usesI2S = (bus.driverType == 1); // getI() updates driverType to reflect the actual resolved driver 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) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index c01a2efe17..5502a25748 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -159,7 +159,7 @@ BusDigital::BusDigital(const BusConfig &bc) if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus // create bus via PolyBus wrapper which will return a WLEDpixelBus::IBus - _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder); + _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType); _valid = (_busPtr != nullptr) && bc.count > 0; // fix for wled#4759 @@ -396,7 +396,7 @@ std::vector BusDigital::getLEDTypes() { } bool BusDigital::isI2S() { - return (_iType & 0x01) == 0; // I2S types have even iType values + return _driverType == 1; // driverType 1 = I2S/LCD } void BusDigital::begin() { @@ -1190,7 +1190,7 @@ size_t BusConfig::memUsage() const { 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); + mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType, driverType); } else if (Bus::isOnOff(type)) { mem += sizeof(BusOnOff); } else { @@ -1259,8 +1259,8 @@ 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); +uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { + return PolyBus::getI(busType, pins, driverType); } //do not call this method from system context (network callback) void BusManager::removeAll() { diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 0a55cf42f9..e317df963c 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -548,7 +548,7 @@ namespace BusManager { void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized void applyABL(); // apply automatic brightness limiter, global or per bus - uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp + uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t& driverType); // workaround for access to PolyBus function from FX_fcn.cpp //do not call this method from system context (network callback) void removeAll(); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 04f861dbb3..73e8ec65c7 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -77,42 +77,31 @@ #define I_8266_BB_SM16825_5 48 /*** ESP32 Neopixel methods ***/ +// iType only encodes LED protocol, driver type (RMT/I2S/LCD) is a separate numeric //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 GRBCW #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 (RGBCW) #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 (RGBCW) #define I_32_RN_SM16825_5 45 -#define I_32_I2_SM16825_5 46 //APA102 #define I_HS_DOT_3 101 //hardware SPI @@ -181,7 +170,7 @@ class PolyBus { return 0; } -static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder) { +static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder, uint8_t driverType = 0) { Serial.printf("[WPB] PolyBus::create busType=%u pin=%u len=%u\n", busType, pins[0], len); if (busType == I_NONE) { Serial.println("[WPB] busType=I_NONE, returning null"); return nullptr; } auto btype = WLEDpixelBus::BusType::Auto; @@ -207,25 +196,36 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, else if (busType == I_8266_DM_NEO_3 || busType == I_8266_DM_400_3 || busType == I_8266_DM_TM1_4 || busType == I_8266_DM_NEO_4 || busType == I_8266_DM_TM2_3 || busType == I_8266_DM_UCS_3 || busType == I_8266_DM_UCS_4 || busType == I_8266_DM_APA106_3 || busType == I_8266_DM_FW6_5 || busType == I_8266_DM_2805_5 || busType == I_8266_DM_TM1914_3 || busType == I_8266_DM_SM16825_5) btype = WLEDpixelBus::BusType::DMA; else btype = WLEDpixelBus::BusType::BitBang; #else - // ESP32: timing selection based on iType (RN=RMT odd, I2=I2S even) + // ESP32: timing selection based on iType (LED protocol only, driver type is separate) switch (busType) { - case I_32_RN_NEO_3: case I_32_I2_NEO_3: timing = WLEDpixelBus::Timing::WS2812; break; - case I_32_RN_NEO_4: case I_32_I2_NEO_4: timing = WLEDpixelBus::Timing::WS2812; break; - case I_32_RN_400_3: case I_32_I2_400_3: timing = WLEDpixelBus::Timing::Generic400Kbps; break; - case I_32_RN_TM1_4: case I_32_I2_TM1_4: timing = WLEDpixelBus::Timing::TM1814; break; - case I_32_RN_TM2_3: case I_32_I2_TM2_3: timing = WLEDpixelBus::Timing::TM1829; break; - case I_32_RN_UCS_3: case I_32_I2_UCS_3: timing = WLEDpixelBus::Timing::UCS8903; break; - case I_32_RN_UCS_4: case I_32_I2_UCS_4: timing = WLEDpixelBus::Timing::UCS8904; break; - case I_32_RN_FW6_5: case I_32_I2_FW6_5: timing = WLEDpixelBus::Timing::FW1906; break; - case I_32_RN_APA106_3: case I_32_I2_APA106_3: timing = WLEDpixelBus::Timing::APA106; break; - case I_32_RN_2805_5: case I_32_I2_2805_5: timing = WLEDpixelBus::Timing::WS2805; break; - case I_32_RN_TM1914_3: case I_32_I2_TM1914_3: timing = WLEDpixelBus::Timing::TM1914; break; - case I_32_RN_SM16825_5: case I_32_I2_SM16825_5: timing = WLEDpixelBus::Timing::SM16825; break; - default: timing = WLEDpixelBus::Timing::WS2812; break; + case I_32_RN_NEO_3: timing = WLEDpixelBus::Timing::WS2812; break; + case I_32_RN_NEO_4: timing = WLEDpixelBus::Timing::WS2812; break; + case I_32_RN_400_3: timing = WLEDpixelBus::Timing::Generic400Kbps; break; + case I_32_RN_TM1_4: timing = WLEDpixelBus::Timing::TM1814; break; + case I_32_RN_TM2_3: timing = WLEDpixelBus::Timing::TM1829; break; + case I_32_RN_UCS_3: timing = WLEDpixelBus::Timing::UCS8903; break; + case I_32_RN_UCS_4: timing = WLEDpixelBus::Timing::UCS8904; break; + case I_32_RN_FW6_5: timing = WLEDpixelBus::Timing::FW1906; break; + case I_32_RN_APA106_3: timing = WLEDpixelBus::Timing::APA106; break; + case I_32_RN_2805_5: timing = WLEDpixelBus::Timing::WS2805; break; + case I_32_RN_TM1914_3: timing = WLEDpixelBus::Timing::TM1914; break; + case I_32_RN_SM16825_5: timing = WLEDpixelBus::Timing::SM16825; break; + default: timing = WLEDpixelBus::Timing::WS2812; break; + } + // ESP32: bus type from explicit driverType parameter (0=RMT, 1=I2S/LCD) + switch (driverType) { + case 0: btype = WLEDpixelBus::BusType::RMT; break; + case 1: + #if defined(CONFIG_IDF_TARGET_ESP32S3) + btype = WLEDpixelBus::BusType::LCD; + #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) + btype = WLEDpixelBus::BusType::I2S; + #else + btype = WLEDpixelBus::BusType::RMT; // C3 has no parallel output + #endif + break; + default: btype = WLEDpixelBus::BusType::RMT; break; } - // ESP32: bus type selection (odd iType = RMT, even = I2S/Auto) - if (busType & 0x01) btype = WLEDpixelBus::BusType::RMT; - // else: leave as Auto (will resolve to I2S or LCD depending on platform) #endif // Assign RMT channel from PolyBus tracking (incremented for each RMT bus) @@ -244,9 +244,9 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, // Determine channel count to parse correct WLEDpixelBus::ColorOrder uint8_t channels = 3; switch (busType) { - case I_32_RN_NEO_4: case I_32_I2_NEO_4: - case I_32_RN_TM1_4: case I_32_I2_TM1_4: - case I_32_RN_UCS_4: case I_32_I2_UCS_4: + case I_32_RN_NEO_4: + case I_32_RN_TM1_4: + case I_32_RN_UCS_4: #ifdef ESP8266 case I_8266_U0_NEO_4: case I_8266_U1_NEO_4: case I_8266_DM_NEO_4: case I_8266_BB_NEO_4: case I_8266_U0_TM1_4: case I_8266_U1_TM1_4: case I_8266_DM_TM1_4: case I_8266_BB_TM1_4: @@ -254,9 +254,9 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #endif channels = 4; break; - case I_32_RN_FW6_5: case I_32_I2_FW6_5: - case I_32_RN_2805_5: case I_32_I2_2805_5: - case I_32_RN_SM16825_5: case I_32_I2_SM16825_5: + case I_32_RN_FW6_5: + case I_32_RN_2805_5: + case I_32_RN_SM16825_5: #ifdef ESP8266 case I_8266_U0_FW6_5: case I_8266_U1_FW6_5: case I_8266_DM_FW6_5: case I_8266_BB_FW6_5: case I_8266_U0_2805_5: case I_8266_U1_2805_5: case I_8266_DM_2805_5: case I_8266_BB_2805_5: @@ -300,7 +300,7 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, [[gnu::hot]] - static unsigned memUsage(unsigned count, unsigned busType) { + static unsigned memUsage(unsigned count, unsigned busType, unsigned driverType = 0) { 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; @@ -341,32 +341,22 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, 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) + // Adjust for channel count and 16-bit types 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_TM1_4 : size += count; break; // 4 channels + case I_32_RN_UCS_3 : size *= 2; break; // 16bit + case I_32_RN_UCS_4 : size = (size + count)*2; break; // 16bit, 4 channels case I_32_RN_FW6_5 : // fallthrough - case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels - case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels - // I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD) - #ifndef CONFIG_IDF_TARGET_ESP32C3 - case I_32_I2_NEO_3 : // fallthrough - case I_32_I2_400_3 : // fallthrough - case I_32_I2_TM2_3 : // fallthrough - case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer - case I_32_I2_NEO_4 : // fallthrough - case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels - case I_32_I2_UCS_3 : size *= 2; break; // 16 bit - case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels - case I_32_I2_FW6_5 : // fallthrough - case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels - case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels - #endif - default : size *= 2; break; // everything else uses 2 buffers + case I_32_RN_2805_5 : size += 2*count; break; // 5 channels + case I_32_RN_SM16825_5: size = (size + 2*count)*2; break; // 16bit, 5 channels + // 3ch types (NEO_3, 400_3, TM2_3, APA106_3, TM1914_3): size stays as count*3 + default : break; #endif } + // ESP32: RMT uses double buffer, I2S/LCD uses single buffer (+ DMA buffer not accounted here) + #ifndef ESP8266 + if (busType != I_NONE && driverType == 0) size *= 2; // RMT: double buffer + #endif return size; } #ifndef ESP8266 @@ -382,7 +372,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #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) { + // driverType is updated to reflect the actual resolved driver (may differ from preference if channels are exhausted) + static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { if (!Bus::isDigital(busType)) return I_NONE; uint8_t t = I_NONE; if (Bus::is2Pin(busType)) { //SPI LED chips @@ -437,54 +428,54 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #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) { + if (driverType == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { _rmtChannelsAssigned++; + // driverType stays 0 (RMT) } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { - offset = 1; // I2S requested or RMT full + driverType = 1; // resolved to I2S/LCD (may differ from preference if RMT was full) _i2sChannelsAssigned++; } else { return I_NONE; // No channels available } - // Now determine actual bus type with the chosen offset + // Determine iType for LED protocol only (no driver encoding) switch (busType) { case TYPE_WS2812_1CH_X3: case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: - t = I_32_RN_NEO_3 + offset; break; + t = I_32_RN_NEO_3; break; case TYPE_SK6812_RGBW: - t = I_32_RN_NEO_4 + offset; break; + t = I_32_RN_NEO_4; break; case TYPE_WS2811_400KHZ: - t = I_32_RN_400_3 + offset; break; + t = I_32_RN_400_3; break; case TYPE_TM1814: - t = I_32_RN_TM1_4 + offset; break; + t = I_32_RN_TM1_4; break; case TYPE_TM1829: - t = I_32_RN_TM2_3 + offset; break; + t = I_32_RN_TM2_3; break; case TYPE_UCS8903: - t = I_32_RN_UCS_3 + offset; break; + t = I_32_RN_UCS_3; break; case TYPE_UCS8904: - t = I_32_RN_UCS_4 + offset; break; + t = I_32_RN_UCS_4; break; case TYPE_APA106: - t = I_32_RN_APA106_3 + offset; break; + t = I_32_RN_APA106_3; break; case TYPE_FW1906: - t = I_32_RN_FW6_5 + offset; break; + t = I_32_RN_FW6_5; break; case TYPE_WS2805: - t = I_32_RN_2805_5 + offset; break; + t = I_32_RN_2805_5; break; case TYPE_TM1914: - t = I_32_RN_TM1914_3 + offset; break; + t = I_32_RN_TM1914_3; break; case TYPE_SM16825: - t = I_32_RN_SM16825_5 + offset; break; + t = I_32_RN_SM16825_5; break; } // If using parallel I2S, set the type accordingly - if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type + if (_i2sChannelsAssigned == 1 && driverType == 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) #endif } - else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag + else if (driverType == 1) { // not first I2S channel, use locked type and enable parallel flag _useParallelI2S = true; t = _parallelBusItype; } From d06ebca54f696ab65b7db136512926606b2e50ce Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 9 Mar 2026 06:27:43 +0100 Subject: [PATCH 007/173] example of adding a new bus type (TM1815) --- wled00/bus_manager.cpp | 3 +- wled00/bus_manager.h | 4 +-- wled00/bus_wrapper.h | 32 +++++++++++++++---- wled00/const.h | 3 +- wled00/data/settings_leds.htm | 2 +- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 1 + 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 5502a25748..c33d8abddc 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -127,7 +127,7 @@ uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { 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) , _iType(bc.iType) // internal bus type determined by getI() @@ -375,6 +375,7 @@ std::vector BusDigital::getLEDTypes() { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, {TYPE_TM1814, "D", PSTR("TM1814")}, + {TYPE_TM1815, "D", PSTR("TM1815")}, {TYPE_WS2811_400KHZ, "D", PSTR("400kHz")}, {TYPE_TM1829, "D", PSTR("TM1829")}, {TYPE_UCS8903, "D", PSTR("UCS8903")}, diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index e317df963c..8f6fa29b4a 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -176,7 +176,7 @@ class Bus { } 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_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 @@ -195,7 +195,7 @@ class Bus { static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } static constexpr bool isHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } - static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } + static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814 || type == TYPE_TM1815; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } static inline int16_t getCCT() { return _cct; } diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 73e8ec65c7..2436dbeb05 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -75,6 +75,11 @@ #define I_8266_U1_SM16825_5 46 #define I_8266_DM_SM16825_5 47 #define I_8266_BB_SM16825_5 48 +//TM1815 (RGBW) +#define I_8266_U0_TM15_4 49 +#define I_8266_U1_TM15_4 50 +#define I_8266_DM_TM15_4 51 +#define I_8266_BB_TM15_4 52 /*** ESP32 Neopixel methods ***/ // iType only encodes LED protocol, driver type (RMT/I2S/LCD) is a separate numeric @@ -102,6 +107,8 @@ #define I_32_RN_TM1914_3 41 //SM16825 (RGBCW) #define I_32_RN_SM16825_5 45 +//TM1815 (RGBW) +#define I_32_RN_TM15_4 49 //APA102 #define I_HS_DOT_3 101 //hardware SPI @@ -190,10 +197,11 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, else if (busType >= I_8266_U0_2805_5 && busType <= I_8266_BB_2805_5) timing = WLEDpixelBus::Timing::WS2805; else if (busType >= I_8266_U0_TM1914_3 && busType <= I_8266_BB_TM1914_3) timing = WLEDpixelBus::Timing::TM1914; else if (busType >= I_8266_U0_SM16825_5 && busType <= I_8266_BB_SM16825_5) timing = WLEDpixelBus::Timing::SM16825; + else if (busType >= I_8266_U0_TM15_4 && busType <= I_8266_BB_TM15_4) timing = WLEDpixelBus::Timing::TM1815; // ESP8266: bus type selection (UART0, UART1, DMA, BitBang) - if (busType == I_8266_U0_NEO_3 || busType == I_8266_U0_400_3 || busType == I_8266_U0_TM1_4 || busType == I_8266_U0_NEO_4 || busType == I_8266_U0_TM2_3 || busType == I_8266_U0_UCS_3 || busType == I_8266_U0_UCS_4 || busType == I_8266_U0_APA106_3 || busType == I_8266_U0_FW6_5 || busType == I_8266_U0_2805_5 || busType == I_8266_U0_TM1914_3 || busType == I_8266_U0_SM16825_5) btype = WLEDpixelBus::BusType::UART; - else if (busType == I_8266_U1_NEO_3 || busType == I_8266_U1_400_3 || busType == I_8266_U1_TM1_4 || busType == I_8266_U1_NEO_4 || busType == I_8266_U1_TM2_3 || busType == I_8266_U1_UCS_3 || busType == I_8266_U1_UCS_4 || busType == I_8266_U1_APA106_3 || busType == I_8266_U1_FW6_5 || busType == I_8266_U1_2805_5 || busType == I_8266_U1_TM1914_3 || busType == I_8266_U1_SM16825_5) btype = WLEDpixelBus::BusType::UART; - else if (busType == I_8266_DM_NEO_3 || busType == I_8266_DM_400_3 || busType == I_8266_DM_TM1_4 || busType == I_8266_DM_NEO_4 || busType == I_8266_DM_TM2_3 || busType == I_8266_DM_UCS_3 || busType == I_8266_DM_UCS_4 || busType == I_8266_DM_APA106_3 || busType == I_8266_DM_FW6_5 || busType == I_8266_DM_2805_5 || busType == I_8266_DM_TM1914_3 || busType == I_8266_DM_SM16825_5) btype = WLEDpixelBus::BusType::DMA; + if (busType == I_8266_U0_NEO_3 || busType == I_8266_U0_400_3 || busType == I_8266_U0_TM1_4 || busType == I_8266_U0_NEO_4 || busType == I_8266_U0_TM2_3 || busType == I_8266_U0_UCS_3 || busType == I_8266_U0_UCS_4 || busType == I_8266_U0_APA106_3 || busType == I_8266_U0_FW6_5 || busType == I_8266_U0_2805_5 || busType == I_8266_U0_TM1914_3 || busType == I_8266_U0_SM16825_5 || busType == I_8266_U0_TM15_4) btype = WLEDpixelBus::BusType::UART; + else if (busType == I_8266_U1_NEO_3 || busType == I_8266_U1_400_3 || busType == I_8266_U1_TM1_4 || busType == I_8266_U1_NEO_4 || busType == I_8266_U1_TM2_3 || busType == I_8266_U1_UCS_3 || busType == I_8266_U1_UCS_4 || busType == I_8266_U1_APA106_3 || busType == I_8266_U1_FW6_5 || busType == I_8266_U1_2805_5 || busType == I_8266_U1_TM1914_3 || busType == I_8266_U1_SM16825_5 || busType == I_8266_U1_TM15_4) btype = WLEDpixelBus::BusType::UART; + else if (busType == I_8266_DM_NEO_3 || busType == I_8266_DM_400_3 || busType == I_8266_DM_TM1_4 || busType == I_8266_DM_NEO_4 || busType == I_8266_DM_TM2_3 || busType == I_8266_DM_UCS_3 || busType == I_8266_DM_UCS_4 || busType == I_8266_DM_APA106_3 || busType == I_8266_DM_FW6_5 || busType == I_8266_DM_2805_5 || busType == I_8266_DM_TM1914_3 || busType == I_8266_DM_SM16825_5 || busType == I_8266_DM_TM15_4) btype = WLEDpixelBus::BusType::DMA; else btype = WLEDpixelBus::BusType::BitBang; #else // ESP32: timing selection based on iType (LED protocol only, driver type is separate) @@ -210,6 +218,7 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, case I_32_RN_2805_5: timing = WLEDpixelBus::Timing::WS2805; break; case I_32_RN_TM1914_3: timing = WLEDpixelBus::Timing::TM1914; break; case I_32_RN_SM16825_5: timing = WLEDpixelBus::Timing::SM16825; break; + case I_32_RN_TM15_4: timing = WLEDpixelBus::Timing::TM1815; break; default: timing = WLEDpixelBus::Timing::WS2812; break; } // ESP32: bus type from explicit driverType parameter (0=RMT, 1=I2S/LCD) @@ -247,10 +256,12 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, case I_32_RN_NEO_4: case I_32_RN_TM1_4: case I_32_RN_UCS_4: + case I_32_RN_TM15_4: #ifdef ESP8266 case I_8266_U0_NEO_4: case I_8266_U1_NEO_4: case I_8266_DM_NEO_4: case I_8266_BB_NEO_4: case I_8266_U0_TM1_4: case I_8266_U1_TM1_4: case I_8266_DM_TM1_4: case I_8266_BB_TM1_4: case I_8266_U0_UCS_4: case I_8266_U1_UCS_4: case I_8266_DM_UCS_4: case I_8266_BB_UCS_4: + case I_8266_U0_TM15_4: case I_8266_U1_TM15_4: case I_8266_DM_TM15_4: case I_8266_BB_TM15_4: #endif channels = 4; break; @@ -311,7 +322,10 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, 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_BB_TM1_4 : // fallthrough + case I_8266_U0_TM15_4 : // fallthrough + case I_8266_U1_TM15_4 : // fallthrough + case I_8266_BB_TM15_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 @@ -334,7 +348,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, 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_TM1_4 : // fallthrough + case I_8266_DM_TM15_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 @@ -343,7 +358,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #else // Adjust for channel count and 16-bit types case I_32_RN_NEO_4 : // fallthrough - case I_32_RN_TM1_4 : size += count; break; // 4 channels + case I_32_RN_TM1_4 : // fallthrough + case I_32_RN_TM15_4 : size += count; break; // 4 channels case I_32_RN_UCS_3 : size *= 2; break; // 16bit case I_32_RN_UCS_4 : size = (size + count)*2; break; // 16bit, 4 channels case I_32_RN_FW6_5 : // fallthrough @@ -424,6 +440,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, t = I_8266_U0_TM1914_3 + offset; break; case TYPE_SM16825: t = I_8266_U0_SM16825_5 + offset; break; + case TYPE_TM1815: + t = I_8266_U0_TM15_4 + offset; break; } #else //ESP32 // dynamic channel allocation based on driver preference @@ -467,6 +485,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, t = I_32_RN_TM1914_3; break; case TYPE_SM16825: t = I_32_RN_SM16825_5; break; + case TYPE_TM1815: + t = I_32_RN_TM15_4; break; } // If using parallel I2S, set the type accordingly if (_i2sChannelsAssigned == 1 && driverType == 1) { // first I2S channel request, lock the type diff --git a/wled00/const.h b/wled00/const.h index 95e69d855b..a681baa744 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -315,10 +315,11 @@ 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) #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/settings_leds.htm b/wled00/data/settings_leds.htm index c10affc545..67bab55e5f 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -325,7 +325,7 @@ setPinConfig(n,t); gId("abl"+n).style.display = (!abl || !isDig(t)) ? "none" : "inline"; // show/hide individual ABL settings if (change) { // did we change LED type? - gId("rf"+n).checked = (gId("rf"+n).checked || t == 31); // LEDs require data in off state (mandatory for TM1814) + gId("rf"+n).checked = (gId("rf"+n).checked || t == 31 || t == 35); // LEDs require data in off state (mandatory for TM1814/TM1815) if (isAna(t)) d.Sf["LC"+n].value = 1; // for sanity change analog count just to 1 LED d.Sf["LA"+n].min = (!isDig(t) || !abl) ? 0 : 1; // set minimum value for LED mA d.Sf["MA"+n].min = (!isDig(t)) ? 0 : 250; // set minimum value for PSU mA diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 3f22770306..57d34ec975 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -35,6 +35,7 @@ namespace Timing { // ---- Titan Micro LEDs ---- constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; // TM1814 RGBW, requires prefix + constexpr LedTiming TM1815 {740, 1780, 1440, 1060, 200}; // TM1815 RGBW, requires prefix constexpr LedTiming TM1829 {300, 900, 800, 400, 200}; // TM1829 (inverted logic) constexpr LedTiming TM1914 {360, 890, 720, 530, 200}; // TM1914 RGB, requires prefix From 17aa057d57ee2c0bce02b94d11ffbde7b502c138 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 9 Mar 2026 07:28:12 +0100 Subject: [PATCH 008/173] simplify bus tracking --- .../rgb-rotary-encoder/rgb-rotary-encoder.cpp | 2 +- wled00/FX_fcn.cpp | 38 +- wled00/bus_manager.cpp | 22 +- wled00/bus_manager.h | 5 +- wled00/bus_wrapper.h | 578 +++++------------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 24 + 6 files changed, 206 insertions(+), 463 deletions(-) diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp index 3ec46f6f77..2d8858ce0a 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp @@ -60,7 +60,7 @@ class RgbRotaryEncoderUsermod : public Usermod // …then set only the LED pin _pins[0] = static_cast(ledIo); BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0); - busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver + BusManager::allocateHardware(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver ledBus = new BusDigital(busCfg); if (!ledBus->isOk()) { cleanup(); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 774a9fde12..8fc628e288 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1173,50 +1173,23 @@ void WS2812FX::finalizeInit() { } } 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 #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; for (auto &bus : busConfigs) { // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels // store the result in iType for later use during bus creation (getI() must only be called once per BusConfig) // note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage - bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType); + BusManager::allocateHardware(bus.type, bus.pins, bus.driverType); } 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) + unsigned busMemUsage = bus.memUsage(); // includes pixel buffers (1x global, 1x segment) + driver memory (e.g. DMA) mem += busMemUsage; - // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) - #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) - bool usesI2S = (bus.driverType == 1); // getI() updates driverType to reflect the actual resolved driver - 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) + + if (mem > 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; @@ -1226,7 +1199,7 @@ void WS2812FX::finalizeInit() { if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count } } - DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem + I2SdmaMem); + DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem); busConfigs.clear(); busConfigs.shrink_to_fit(); @@ -2077,3 +2050,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", "Candy2","Traffic Light" ])====="; + diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index c33d8abddc..8c3cb7310b 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -130,14 +130,12 @@ BusDigital::BusDigital(const BusConfig &bc) : 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) -, _iType(bc.iType) // internal bus type determined by getI() , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) , _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } - if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } _frequencykHz = 0U; _colorSum = 0; @@ -159,7 +157,7 @@ BusDigital::BusDigital(const BusConfig &bc) if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus // create bus via PolyBus wrapper which will return a WLEDpixelBus::IBus - _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType); + _busPtr = PolyBus::create(bc.type, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType); _valid = (_busPtr != nullptr) && bc.count > 0; // fix for wled#4759 @@ -171,12 +169,11 @@ BusDigital::BusDigital(const BusConfig &bc) } else { 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", (int)_milliAmpsPerLed, (int)_milliAmpsMax, _valid ? " " : "FAILED" @@ -412,7 +409,6 @@ void BusDigital::cleanup() { delete _busPtr; _busPtr = nullptr; } - _iType = I_NONE; _valid = false; _busPtr = nullptr; PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); @@ -800,7 +796,7 @@ void BusNetwork::cleanup() { DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); d_free(_data); _data = nullptr; - _type = I_NONE; + _type = TYPE_NONE; _valid = false; } @@ -1191,7 +1187,7 @@ size_t BusConfig::memUsage() const { 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, driverType); + mem += sizeof(BusDigital) + PolyBus::memUsage(type, count + skipAmount, pins, driverType); } else if (Bus::isOnOff(type)) { mem += sizeof(BusOnOff); } else { @@ -1260,8 +1256,8 @@ String BusManager::getLEDTypesJSONString() { return json; } -uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { - return PolyBus::getI(busType, pins, driverType); +bool BusManager::allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { + return PolyBus::allocateHardware(busType, pins, driverType); } //do not call this method from system context (network callback) void BusManager::removeAll() { @@ -1465,12 +1461,11 @@ ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } #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::_i2sChannelsAssigned = 0; uint8_t PolyBus::_2PchannelsAssigned = 0; +uint8_t PolyBus::_parallelI2sBusType = 0; #endif // Bus static member definition int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin() @@ -1483,3 +1478,4 @@ std::vector> BusManager::busses; uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; bool BusManager::_useABL = false; + diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 8f6fa29b4a..863a89b4ec 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -277,7 +277,6 @@ class BusDigital : public Bus { 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; @@ -464,7 +463,6 @@ struct BusConfig { 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 = "") @@ -478,7 +476,6 @@ struct BusConfig { , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) , driverType(driver) - , iType(0) // default to I_NONE , text(sometext) { refreshReq = (bool) GET_BIT(busType,7); @@ -548,7 +545,7 @@ namespace BusManager { void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized void applyABL(); // apply automatic brightness limiter, global or per bus - uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t& driverType); // workaround for access to PolyBus function from FX_fcn.cpp + bool allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType); // workaround to access PolyBus function //do not call this method from system context (network callback) void removeAll(); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 2436dbeb05..32f93d3365 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,9 +1,10 @@ -#pragma once +#pragma once #ifndef BusWrapper_h #define BusWrapper_h //#define NPB_CONF_4STEP_CADENCE #include "src/WLEDpixelBus/WLEDpixelBus.h" +#include "src/WLEDpixelBus/WLEDpixelBus_SPI.h" //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -13,150 +14,65 @@ #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 GRBCW -#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 (RGBCW) -#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 (RGBCW) -#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 -//TM1815 (RGBW) -#define I_8266_U0_TM15_4 49 -#define I_8266_U1_TM15_4 50 -#define I_8266_DM_TM15_4 51 -#define I_8266_BB_TM15_4 52 - -/*** ESP32 Neopixel methods ***/ -// iType only encodes LED protocol, driver type (RMT/I2S/LCD) is a separate numeric -//RGB -#define I_32_RN_NEO_3 1 -//RGBW -#define I_32_RN_NEO_4 5 -//400Kbps -#define I_32_RN_400_3 9 -//TM1814 (RGBW) -#define I_32_RN_TM1_4 13 -//TM1829 (RGB) -#define I_32_RN_TM2_3 17 -//UCS8903 (RGB) -#define I_32_RN_UCS_3 21 -//UCS8904 (RGBW) -#define I_32_RN_UCS_4 25 -//FW1906 GRBCW -#define I_32_RN_FW6_5 29 -//APA106 -#define I_32_RN_APA106_3 33 -//WS2805 (RGBCW) -#define I_32_RN_2805_5 37 -//TM1914 (RGB) -#define I_32_RN_TM1914_3 41 -//SM16825 (RGBCW) -#define I_32_RN_SM16825_5 45 -//TM1815 (RGBW) -#define I_32_RN_TM15_4 49 - -//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 +struct ProtocolDef { + WLEDpixelBus::LedTiming timing; + uint8_t channels; + bool is16bit; +}; -//WS2801 -#define I_HS_WS1_3 105 -#define I_SS_WS1_3 106 +// Maps WLED's internal TYPE_ to actual timings and capabilities +inline ProtocolDef getProtocol(uint8_t wledType) { + switch (wledType) { + // 3 channels + case TYPE_WS2812_1CH_X3: + case TYPE_WS2812_2CH_X3: + case TYPE_WS2812_RGB: + case TYPE_WS2812_WWA: return { WLEDpixelBus::Timing::WS2812, 3, false }; + case TYPE_WS2811_400KHZ:return { WLEDpixelBus::Timing::Generic400Kbps, 3, false }; + case TYPE_TM1829: return { WLEDpixelBus::Timing::TM1829, 3, false }; + case TYPE_UCS8903: return { WLEDpixelBus::Timing::UCS8903, 3, true }; // 16bit + case TYPE_APA106: return { WLEDpixelBus::Timing::APA106, 3, false }; + case TYPE_TM1914: return { WLEDpixelBus::Timing::TM1914, 3, false }; -//P9813 -#define I_HS_P98_3 107 -#define I_SS_P98_3 108 + // 4 channels + case TYPE_SK6812_RGBW: return { WLEDpixelBus::Timing::SK6812, 4, false }; + case TYPE_TM1814: return { WLEDpixelBus::Timing::TM1814, 4, false }; + case TYPE_UCS8904: return { WLEDpixelBus::Timing::UCS8904, 4, true }; // 16bit + case TYPE_TM1815: return { WLEDpixelBus::Timing::TM1815, 4, false }; -//LPD6803 -#define I_HS_LPO_3 109 -#define I_SS_LPO_3 110 + // 5 channels + case TYPE_FW1906: return { WLEDpixelBus::Timing::FW1906, 5, false }; + case TYPE_WS2805: return { WLEDpixelBus::Timing::WS2805, 5, false }; + case TYPE_SM16825: return { WLEDpixelBus::Timing::SM16825, 5, true }; // 16bit + + // SPI specific + case TYPE_APA102: return { WLEDpixelBus::Timing::APA102, 3, false }; + case TYPE_LPD8806: return { WLEDpixelBus::Timing::LPD8806, 3, false }; + case TYPE_WS2801: return { WLEDpixelBus::Timing::WS2801, 3, false }; + case TYPE_P9813: return { WLEDpixelBus::Timing::P9813, 3, false }; + case TYPE_LPD6803: return { WLEDpixelBus::Timing::LPD6803, 3, false }; -// 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 + default: return { WLEDpixelBus::Timing::WS2812, 3, false }; + } +} 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. + static uint8_t _rmtChannelsAssigned; + static uint8_t _rmtChannel; + static uint8_t _i2sChannelsAssigned; + static uint8_t _2PchannelsAssigned; + static uint8_t _parallelI2sBusType; // Track first I2S type to enforce parallel timing #endif public: - // initialize SPI bus speed for DotStar methods template - static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz) { if (busPtr) static_cast(busPtr)->begin(); } static void* cleanup(void* busPtr, uint8_t busType) { - if (busPtr) { - delete static_cast(busPtr); - } + if (busPtr) delete static_cast(busPtr); return nullptr; } static void show(void* busPtr, uint8_t busType, bool isI2s = false) { @@ -177,51 +93,101 @@ class PolyBus { return 0; } + static void resetChannelTracking() { + #ifndef ESP8266 + _rmtChannelsAssigned = 0; + _rmtChannel = 0; + _i2sChannelsAssigned = 0; + _2PchannelsAssigned = 0; + _parallelI2sBusType = 0; // TYPE_NONE + WLEDpixelBus::RmtBus::resetAutoChannel(); + #endif + } + + static bool allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { + if (!Bus::isDigital(busType)) return false; + + if (Bus::is2Pin(busType)) { + #ifndef ESP8266 + _2PchannelsAssigned++; + #endif + return true; // SPI uses separate pins + } + + #ifndef ESP8266 + if (driverType == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { + _rmtChannelsAssigned++; + } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { + driverType = 1; + // If first I2S channel request, lock the type to ensure parallel timings match + if (_i2sChannelsAssigned == 0) { + _parallelI2sBusType = busType; + } + _i2sChannelsAssigned++; + } else { + return false; // No channels available + } + #endif + + return true; + } + static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder, uint8_t driverType = 0) { - Serial.printf("[WPB] PolyBus::create busType=%u pin=%u len=%u\n", busType, pins[0], len); - if (busType == I_NONE) { Serial.println("[WPB] busType=I_NONE, returning null"); return nullptr; } + if (!Bus::isDigital(busType)) return nullptr; + + #ifndef ESP8266 + if (driverType == 1 && _parallelI2sBusType != 0) { + busType = _parallelI2sBusType; // Lock type for hardware timing sync + } + #endif + + ProtocolDef proto = getProtocol(busType); + + // Map WLED Order + uint8_t wledOrder = (uint8_t)colorOrder & 0x0F; + WLEDpixelBus::ColorOrder finalOrder = WLEDpixelBus::ColorOrder::GRB; + + if (proto.channels >= 4) { + switch (wledOrder) { + case 0: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; + case 1: finalOrder = WLEDpixelBus::ColorOrder::RGBW; break; + case 2: finalOrder = WLEDpixelBus::ColorOrder::BRGW; break; + case 3: finalOrder = WLEDpixelBus::ColorOrder::RBGW; break; + case 4: finalOrder = WLEDpixelBus::ColorOrder::BGRW; break; + case 5: finalOrder = WLEDpixelBus::ColorOrder::GBRW; break; + default: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; + } + } else { + switch (wledOrder) { + case 0: finalOrder = WLEDpixelBus::ColorOrder::GRB; break; + case 1: finalOrder = WLEDpixelBus::ColorOrder::RGB; break; + case 2: finalOrder = WLEDpixelBus::ColorOrder::BRG; break; + case 3: finalOrder = WLEDpixelBus::ColorOrder::RBG; break; + case 4: finalOrder = WLEDpixelBus::ColorOrder::BGR; break; + case 5: finalOrder = WLEDpixelBus::ColorOrder::GBR; break; + default: finalOrder = WLEDpixelBus::ColorOrder::GRB; break; + } + } + + if (Bus::is2Pin(busType)) { + bool isHSPI = false; + #ifdef ESP8266 + if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; + #else + if (_2PchannelsAssigned == 1) isHSPI = true; + #endif + return new WLEDpixelBus::SpiBus(pins[0], pins[1], proto.timing, finalOrder, isHSPI); + } + auto btype = WLEDpixelBus::BusType::Auto; - auto timing = WLEDpixelBus::Timing::WS2812; uint8_t hwIndex = pins[0]; #ifdef ESP8266 - // ESP8266: timing selection based on iType ranges - if (busType >= I_8266_U0_NEO_3 && busType <= I_8266_BB_NEO_3) timing = WLEDpixelBus::Timing::WS2812; - else if (busType >= I_8266_U0_400_3 && busType <= I_8266_BB_400_3) timing = WLEDpixelBus::Timing::Generic400Kbps; - else if (busType >= I_8266_U0_TM1_4 && busType <= I_8266_BB_TM1_4) timing = WLEDpixelBus::Timing::TM1814; - else if (busType >= I_8266_U0_TM2_3 && busType <= I_8266_BB_TM2_3) timing = WLEDpixelBus::Timing::TM1829; - else if (busType >= I_8266_U0_UCS_3 && busType <= I_8266_BB_UCS_3) timing = WLEDpixelBus::Timing::UCS8903; - else if (busType >= I_8266_U0_UCS_4 && busType <= I_8266_BB_UCS_4) timing = WLEDpixelBus::Timing::UCS8904; - else if (busType >= I_8266_U0_APA106_3 && busType <= I_8266_BB_APA106_3) timing = WLEDpixelBus::Timing::APA106; - else if (busType >= I_8266_U0_FW6_5 && busType <= I_8266_BB_FW6_5) timing = WLEDpixelBus::Timing::FW1906; - else if (busType >= I_8266_U0_2805_5 && busType <= I_8266_BB_2805_5) timing = WLEDpixelBus::Timing::WS2805; - else if (busType >= I_8266_U0_TM1914_3 && busType <= I_8266_BB_TM1914_3) timing = WLEDpixelBus::Timing::TM1914; - else if (busType >= I_8266_U0_SM16825_5 && busType <= I_8266_BB_SM16825_5) timing = WLEDpixelBus::Timing::SM16825; - else if (busType >= I_8266_U0_TM15_4 && busType <= I_8266_BB_TM15_4) timing = WLEDpixelBus::Timing::TM1815; - // ESP8266: bus type selection (UART0, UART1, DMA, BitBang) - if (busType == I_8266_U0_NEO_3 || busType == I_8266_U0_400_3 || busType == I_8266_U0_TM1_4 || busType == I_8266_U0_NEO_4 || busType == I_8266_U0_TM2_3 || busType == I_8266_U0_UCS_3 || busType == I_8266_U0_UCS_4 || busType == I_8266_U0_APA106_3 || busType == I_8266_U0_FW6_5 || busType == I_8266_U0_2805_5 || busType == I_8266_U0_TM1914_3 || busType == I_8266_U0_SM16825_5 || busType == I_8266_U0_TM15_4) btype = WLEDpixelBus::BusType::UART; - else if (busType == I_8266_U1_NEO_3 || busType == I_8266_U1_400_3 || busType == I_8266_U1_TM1_4 || busType == I_8266_U1_NEO_4 || busType == I_8266_U1_TM2_3 || busType == I_8266_U1_UCS_3 || busType == I_8266_U1_UCS_4 || busType == I_8266_U1_APA106_3 || busType == I_8266_U1_FW6_5 || busType == I_8266_U1_2805_5 || busType == I_8266_U1_TM1914_3 || busType == I_8266_U1_SM16825_5 || busType == I_8266_U1_TM15_4) btype = WLEDpixelBus::BusType::UART; - else if (busType == I_8266_DM_NEO_3 || busType == I_8266_DM_400_3 || busType == I_8266_DM_TM1_4 || busType == I_8266_DM_NEO_4 || busType == I_8266_DM_TM2_3 || busType == I_8266_DM_UCS_3 || busType == I_8266_DM_UCS_4 || busType == I_8266_DM_APA106_3 || busType == I_8266_DM_FW6_5 || busType == I_8266_DM_2805_5 || busType == I_8266_DM_TM1914_3 || busType == I_8266_DM_SM16825_5 || busType == I_8266_DM_TM15_4) btype = WLEDpixelBus::BusType::DMA; + uint8_t offset = pins[0] - 1; + if (offset == 0 || offset == 1) btype = WLEDpixelBus::BusType::UART; + else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; else btype = WLEDpixelBus::BusType::BitBang; #else - // ESP32: timing selection based on iType (LED protocol only, driver type is separate) - switch (busType) { - case I_32_RN_NEO_3: timing = WLEDpixelBus::Timing::WS2812; break; - case I_32_RN_NEO_4: timing = WLEDpixelBus::Timing::WS2812; break; - case I_32_RN_400_3: timing = WLEDpixelBus::Timing::Generic400Kbps; break; - case I_32_RN_TM1_4: timing = WLEDpixelBus::Timing::TM1814; break; - case I_32_RN_TM2_3: timing = WLEDpixelBus::Timing::TM1829; break; - case I_32_RN_UCS_3: timing = WLEDpixelBus::Timing::UCS8903; break; - case I_32_RN_UCS_4: timing = WLEDpixelBus::Timing::UCS8904; break; - case I_32_RN_FW6_5: timing = WLEDpixelBus::Timing::FW1906; break; - case I_32_RN_APA106_3: timing = WLEDpixelBus::Timing::APA106; break; - case I_32_RN_2805_5: timing = WLEDpixelBus::Timing::WS2805; break; - case I_32_RN_TM1914_3: timing = WLEDpixelBus::Timing::TM1914; break; - case I_32_RN_SM16825_5: timing = WLEDpixelBus::Timing::SM16825; break; - case I_32_RN_TM15_4: timing = WLEDpixelBus::Timing::TM1815; break; - default: timing = WLEDpixelBus::Timing::WS2812; break; - } - // ESP32: bus type from explicit driverType parameter (0=RMT, 1=I2S/LCD) switch (driverType) { case 0: btype = WLEDpixelBus::BusType::RMT; break; case 1: @@ -230,278 +196,64 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) btype = WLEDpixelBus::BusType::I2S; #else - btype = WLEDpixelBus::BusType::RMT; // C3 has no parallel output + btype = WLEDpixelBus::BusType::RMT; #endif break; default: btype = WLEDpixelBus::BusType::RMT; break; } #endif - // Assign RMT channel from PolyBus tracking (incremented for each RMT bus) - int8_t rmtCh = -1; // default: let WLEDpixelBus auto-allocate + int8_t rmtCh = -1; #ifndef ESP8266 if (btype == WLEDpixelBus::BusType::RMT) { if (_rmtChannel < WLED_MAX_RMT_CHANNELS) { rmtCh = _rmtChannel++; } else { - Serial.printf("[WPB] ERROR: no RMT channels left (max=%u)\n", WLED_MAX_RMT_CHANNELS); return nullptr; } } #endif - // Determine channel count to parse correct WLEDpixelBus::ColorOrder - uint8_t channels = 3; - switch (busType) { - case I_32_RN_NEO_4: - case I_32_RN_TM1_4: - case I_32_RN_UCS_4: - case I_32_RN_TM15_4: - #ifdef ESP8266 - case I_8266_U0_NEO_4: case I_8266_U1_NEO_4: case I_8266_DM_NEO_4: case I_8266_BB_NEO_4: - case I_8266_U0_TM1_4: case I_8266_U1_TM1_4: case I_8266_DM_TM1_4: case I_8266_BB_TM1_4: - case I_8266_U0_UCS_4: case I_8266_U1_UCS_4: case I_8266_DM_UCS_4: case I_8266_BB_UCS_4: - case I_8266_U0_TM15_4: case I_8266_U1_TM15_4: case I_8266_DM_TM15_4: case I_8266_BB_TM15_4: - #endif - channels = 4; - break; - case I_32_RN_FW6_5: - case I_32_RN_2805_5: - case I_32_RN_SM16825_5: - #ifdef ESP8266 - case I_8266_U0_FW6_5: case I_8266_U1_FW6_5: case I_8266_DM_FW6_5: case I_8266_BB_FW6_5: - case I_8266_U0_2805_5: case I_8266_U1_2805_5: case I_8266_DM_2805_5: case I_8266_BB_2805_5: - case I_8266_U0_SM16825_5: case I_8266_U1_SM16825_5: case I_8266_DM_SM16825_5: case I_8266_BB_SM16825_5: - #endif - channels = 5; - break; - } + return WLEDpixelBus::createBus(btype, hwIndex, proto.timing, finalOrder, len, rmtCh); + } - // Map WLED UI order (0-5) to WLEDpixelBus order (which natively handles byte extraction) - uint8_t wledOrder = (uint8_t)colorOrder & 0x0F; - WLEDpixelBus::ColorOrder finalOrder = WLEDpixelBus::ColorOrder::GRB; - - if (channels >= 4) { - switch (wledOrder) { - case 0: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; - case 1: finalOrder = WLEDpixelBus::ColorOrder::RGBW; break; - case 2: finalOrder = WLEDpixelBus::ColorOrder::BRGW; break; - case 3: finalOrder = WLEDpixelBus::ColorOrder::RBGW; break; - case 4: finalOrder = WLEDpixelBus::ColorOrder::BGRW; break; // Note WLED's 4 is BGR, so we explicitly select BGRW - case 5: finalOrder = WLEDpixelBus::ColorOrder::GBRW; break; // Note WLED's 5 is GBR, so we explicitly select GBRW - default: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; - } - } else { - switch (wledOrder) { - case 0: finalOrder = WLEDpixelBus::ColorOrder::GRB; break; - case 1: finalOrder = WLEDpixelBus::ColorOrder::RGB; break; - case 2: finalOrder = WLEDpixelBus::ColorOrder::BRG; break; - case 3: finalOrder = WLEDpixelBus::ColorOrder::RBG; break; - case 4: finalOrder = WLEDpixelBus::ColorOrder::BGR; break; // WLED 4 is BGR, WPB enum for BGR is 5 but explicit is safer - case 5: finalOrder = WLEDpixelBus::ColorOrder::GBR; break; // WLED 5 is GBR, WPB enum for GBR is 4 but explicit is safer - default: finalOrder = WLEDpixelBus::ColorOrder::GRB; break; - } + static unsigned memUsage(uint8_t busType, unsigned count, const uint8_t* pins, unsigned driverType = 0) { + if (!Bus::isDigital(busType)) return 0; + + #ifndef ESP8266 + if (driverType == 1 && _parallelI2sBusType != 0) { + busType = _parallelI2sBusType; // Ensure memory matches the hardware configuration } + #endif - Serial.printf("[WPB] resolved: btype=%u timing_t0h=%u pin=%u rmtCh=%d\n", (unsigned)btype, timing.t0h_ns, hwIndex, rmtCh); - return WLEDpixelBus::createBus(btype, hwIndex, timing, finalOrder, len, rmtCh); - } + ProtocolDef proto = getProtocol(busType); - [[gnu::hot]] + if (Bus::is2Pin(busType)) { + // Basic buffer overhead for SPI + return WLEDpixelBus::estimateMemory(WLEDpixelBus::BusType::Auto, count, proto.channels); + } - [[gnu::hot]] + auto btype = WLEDpixelBus::BusType::Auto; - static unsigned memUsage(unsigned count, unsigned busType, unsigned driverType = 0) { - 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 : // fallthrough - case I_8266_U0_TM15_4 : // fallthrough - case I_8266_U1_TM15_4 : // fallthrough - case I_8266_BB_TM15_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 : // fallthrough - case I_8266_DM_TM15_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; + btype = WLEDpixelBus::BusType::Auto; #else - // Adjust for channel count and 16-bit types - case I_32_RN_NEO_4 : // fallthrough - case I_32_RN_TM1_4 : // fallthrough - case I_32_RN_TM15_4 : size += count; break; // 4 channels - case I_32_RN_UCS_3 : size *= 2; break; // 16bit - case I_32_RN_UCS_4 : size = (size + count)*2; break; // 16bit, 4 channels - case I_32_RN_FW6_5 : // fallthrough - case I_32_RN_2805_5 : size += 2*count; break; // 5 channels - case I_32_RN_SM16825_5: size = (size + 2*count)*2; break; // 16bit, 5 channels - // 3ch types (NEO_3, 400_3, TM2_3, APA106_3, TM1914_3): size stays as count*3 - default : break; - #endif - } - // ESP32: RMT uses double buffer, I2S/LCD uses single buffer (+ DMA buffer not accounted here) - #ifndef ESP8266 - if (busType != I_NONE && driverType == 0) size *= 2; // RMT: double buffer - #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; - WLEDpixelBus::RmtBus::resetAutoChannel(); // also reset WLEDpixelBus internal counter - } -#endif - // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins - // driverType is updated to reflect the actual resolved driver (may differ from preference if channels are exhausted) - static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { - if (!Bus::isDigital(busType)) return I_NONE; - uint8_t t = I_NONE; - if (Bus::is2Pin(busType)) { //SPI LED chips - 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_2CH_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; - case TYPE_TM1815: - t = I_8266_U0_TM15_4 + 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) - if (driverType == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { - _rmtChannelsAssigned++; - // driverType stays 0 (RMT) - } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { - driverType = 1; // resolved to I2S/LCD (may differ from preference if RMT was full) - _i2sChannelsAssigned++; - } else { - return I_NONE; // No channels available + switch (driverType) { + case 0: btype = WLEDpixelBus::BusType::RMT; break; + case 1: + #if defined(CONFIG_IDF_TARGET_ESP32S3) + btype = WLEDpixelBus::BusType::LCD; + #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) + btype = WLEDpixelBus::BusType::I2S; + #else + btype = WLEDpixelBus::BusType::RMT; + #endif + break; + default: btype = WLEDpixelBus::BusType::RMT; break; } + #endif - // Determine iType for LED protocol only (no driver encoding) - switch (busType) { - case TYPE_WS2812_1CH_X3: - case TYPE_WS2812_2CH_X3: - case TYPE_WS2812_RGB: - case TYPE_WS2812_WWA: - t = I_32_RN_NEO_3; break; - case TYPE_SK6812_RGBW: - t = I_32_RN_NEO_4; break; - case TYPE_WS2811_400KHZ: - t = I_32_RN_400_3; break; - case TYPE_TM1814: - t = I_32_RN_TM1_4; break; - case TYPE_TM1829: - t = I_32_RN_TM2_3; break; - case TYPE_UCS8903: - t = I_32_RN_UCS_3; break; - case TYPE_UCS8904: - t = I_32_RN_UCS_4; break; - case TYPE_APA106: - t = I_32_RN_APA106_3; break; - case TYPE_FW1906: - t = I_32_RN_FW6_5; break; - case TYPE_WS2805: - t = I_32_RN_2805_5; break; - case TYPE_TM1914: - t = I_32_RN_TM1914_3; break; - case TYPE_SM16825: - t = I_32_RN_SM16825_5; break; - case TYPE_TM1815: - t = I_32_RN_TM15_4; break; - } - // If using parallel I2S, set the type accordingly - if (_i2sChannelsAssigned == 1 && driverType == 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) - #endif - } - else if (driverType == 1) { // not first I2S channel, use locked type and enable parallel flag - _useParallelI2S = true; - t = _parallelBusItype; - } - #endif - } - return t; + return WLEDpixelBus::estimateMemory(btype, count, proto.channels); } }; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index b662ab2532..3039fb1902 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -680,4 +680,28 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, */ BusType getRecommendedBusType(); +/** + * Estimate exact memory footprint of a bus. + * Accounts for encode buffers, driver overhead, and shared DMA contexts. + */ +static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t channelCount, size_t dmaBufferSize = DEFAULT_DMA_BUFFER_SIZE) { + size_t mem = 0; + + // Bus instance overhead + mem += 128; // Approximate C++ object size + IBus data structures + + // Encode buffer (allocated continuously across RMT, I2S, LCD) + mem += numPixels * channelCount; + + // DMA buffers and descriptors for I2S/LCD + // They are double-buffered and mapped via heap_caps_malloc + if (type == BusType::I2S || type == BusType::LCD || type == BusType::Auto) { + mem += dmaBufferSize * 2; + // Include minimal descriptor memory estimates (~64 bytes per context) + mem += 64; + } + + return mem; +} + } // namespace WLEDpixelBus \ No newline at end of file From a8c91f31d031651060a4ac3e73feaa0259835164 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 16:31:33 +0100 Subject: [PATCH 009/173] fixes for I2S (almost working) add SPI for C3 (untested) --- wled00/bus_wrapper.h | 2 +- wled00/data/index.htm | 1 + wled00/data/index.js | 2 +- wled00/data/settings_leds.htm | 1 + wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 804 +++++++++++++++++++---- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 127 +++- 6 files changed, 805 insertions(+), 132 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 32f93d3365..0f6dac3c28 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -214,7 +214,7 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #endif - return WLEDpixelBus::createBus(btype, hwIndex, proto.timing, finalOrder, len, rmtCh); + return WLEDpixelBus::createBus(btype, hwIndex, proto.timing, finalOrder, WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, rmtCh); } static unsigned memUsage(uint8_t busType, unsigned count, const uint8_t* pins, unsigned driverType = 0) { diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 4d680cfbe2..b878e82e18 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -370,3 +370,4 @@ + \ No newline at end of file diff --git a/wled00/data/index.js b/wled00/data/index.js index cd508e8642..b1b174e31a 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3465,4 +3465,4 @@ _C.addEventListener('touchstart', lock, false); _C.addEventListener('mouseout', move, false); _C.addEventListener('mouseup', move, false); -_C.addEventListener('touchend', move, false); \ No newline at end of file +_C.addEventListener('touchend', move, false); \ No newline at end of file diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 67bab55e5f..e304f087b0 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -1184,3 +1184,4 @@

Advanced

+ \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 84d8975619..dd37571b31 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -414,17 +414,17 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { if (_initialized) return true; _timing = timing; - _bufferSize = bufferSize; + _bufferSize = (bufferSize + 3) & ~3; // align to 4 bytes - // Allocate DMA buffers + // Allocate DMA buffers (4-byte aligned for DMA) for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_malloc(bufferSize, MALLOC_CAP_DMA); + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); if (!_dmaBuffer[i]) { log_e("I2S DMA buffer alloc failed"); deinit(); return false; } - memset(_dmaBuffer[i], 0, bufferSize); + memset(_dmaBuffer[i], 0, _bufferSize); _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); if (!_dmaDesc[i]) { @@ -434,16 +434,17 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { } } - // Setup DMA descriptors - linked list for ping-pong + // Setup DMA descriptors - circular chain for gapless ping-pong for (int i = 0; i < 2; i++) { - _dmaDesc[i]->size = bufferSize; - _dmaDesc[i]->length = bufferSize; + _dmaDesc[i]->size = _bufferSize; + _dmaDesc[i]->length = _bufferSize; _dmaDesc[i]->buf = _dmaBuffer[i]; _dmaDesc[i]->eof = 1; // Generate interrupt on completion _dmaDesc[i]->sosf = 0; _dmaDesc[i]->owner = 1; - _dmaDesc[i]->qe.stqe_next = nullptr; // Set dynamically } + _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; + _dmaDesc[1]->qe.stqe_next = _dmaDesc[0]; // Enable I2S peripheral #if defined(WLEDPB_ESP32) @@ -452,56 +453,133 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { 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 + // Configure for 8-bit parallel LCD mode (matching NeoPixelBus) + _i2sDev->conf2.val = 0; _i2sDev->conf2.lcd_en = 1; - _i2sDev->conf2.lcd_tx_wrx2_en = 0; + _i2sDev->conf2.lcd_tx_wrx2_en = 1; // Required for 8-bit parallel output _i2sDev->conf2.lcd_tx_sdx2_en = 0; - // Calculate clock divider for 4-step cadence - // Bit period / 4 = step time + // DMA config + _i2sDev->lc_conf.val = 0; + _i2sDev->lc_conf.out_eof_mode = 1; + + // Disable PDM +#if defined(WLEDPB_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 = 1; // 16-bit single channel + _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold + + // PCM bypass + _i2sDev->conf1.val = 0; + _i2sDev->conf1.tx_stop_en = 0; + _i2sDev->conf1.tx_pcm_bypass = 1; + + // Channel config + _i2sDev->conf_chan.val = 0; + _i2sDev->conf_chan.tx_chan_mod = 1; // Right channel + + // I2S conf + _i2sDev->conf.val = 0; + _i2sDev->conf.tx_msb_shift = 0; // No shift in parallel mode + _i2sDev->conf.tx_right_first = 1; + + // Clear timing register + _i2sDev->timing.val = 0; + + // Calculate clock divider for 4-step cadence (matching NeoPixelBus) + // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) + // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns + // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) + const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 uint32_t bitPeriodNs = timing.bitPeriod(); - uint32_t stepPeriodNs = bitPeriodNs / 4; #if defined(WLEDPB_ESP32) - uint32_t baseClock = 160000000; + const double baseClockMhz = 160.0; #else - uint32_t baseClock = 80000000; + const double baseClockMhz = 80.0; #endif - _clockDiv = (baseClock / 1000000) * stepPeriodNs / 1000; - if (_clockDiv < 2) _clockDiv = 2; - if (_clockDiv > 255) _clockDiv = 255; + // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz + // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 + double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; + if (clkmdiv < 2.0) clkmdiv = 2.0; + if (clkmdiv > 255.0) clkmdiv = 255.0; + + uint8_t clkmInteger = (uint8_t)clkmdiv; + double clkmFraction = clkmdiv - clkmInteger; - // Set clock - _i2sDev->clkm_conf.clkm_div_a = 0; - _i2sDev->clkm_conf.clkm_div_b = 0; - _i2sDev->clkm_conf.clkm_div_num = _clockDiv; + // Convert fraction to divB/divA (fraction = divB/divA) + uint8_t divB = 0; + uint8_t divA = 0; + if (clkmFraction > 0.001) { + divA = 63; // use max denominator for best precision + divB = (uint8_t)(clkmFraction * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + _clockDiv = clkmInteger; + + Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", + bitPeriodNs, clkmInteger, divB, divA, bckDiv); + double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; + if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; + Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", + actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); + + // Set clock (with fractional divider for accurate timing) + _i2sDev->clkm_conf.val = 0; _i2sDev->clkm_conf.clk_en = 1; + _i2sDev->clkm_conf.clkm_div_a = divA; + _i2sDev->clkm_conf.clkm_div_b = divB; + _i2sDev->clkm_conf.clkm_div_num = clkmInteger; - // Sample configuration - _i2sDev->fifo_conf.tx_fifo_mod = 1; // 8-bit parallel - _i2sDev->fifo_conf.tx_fifo_mod_force_en = 1; - _i2sDev->sample_rate_conf.tx_bck_div_num = 1; + // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) + _i2sDev->sample_rate_conf.val = 0; + _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; _i2sDev->sample_rate_conf.tx_bits_mod = 8; - // Channel config - _i2sDev->conf_chan.tx_chan_mod = 1; - _i2sDev->conf.tx_right_first = 1; - _i2sDev->conf.tx_msb_right = 0; - _i2sDev->conf.tx_mono = 1; - - // Enable DMA - _i2sDev->fifo_conf.dscr_en = 1; - _i2sDev->lc_conf.out_data_burst_en = 1; - _i2sDev->lc_conf.outdscr_burst_en = 1; + // 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; @@ -521,14 +599,17 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { } _initialized = true; + Serial.printf("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); return true; } void I2sBusContext::deinit() { if (_i2sDev) { + _i2sDev->int_ena.val = 0; // Disable interrupts first _i2sDev->conf.tx_start = 0; _i2sDev->out_link.start = 0; } + _state = DriverState::Idle; if (_isrHandle) { esp_intr_free(_isrHandle); @@ -615,42 +696,48 @@ void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ } } -void I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, size_t* bytesWritten) { +void I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { // 4-step cadence encoding for parallel output // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) - // Pattern: [1][data][data][0] for each bit + // Desired output: [HIGH][data][data][LOW] for each bit + // + // ESP32 I2S LCD mode with lcd_tx_wrx2_en=1 swaps half-words within 32-bit values: + // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] + // (NeoPixelBus documents this as "bytes within the words are swapped") + // So we write: p[0]=step2, p[1]=step3, p[2]=step0, p[3]=step1 // - // For multiple channels, we interleave bits into each byte position + // Buffer is always filled completely (zeros = LOW = reset signal) - size_t written = 0; memset(dest, 0, destLen); + size_t pos = 0; // Process each source byte position across all channels - while (written + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte + while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte bool hasData = false; for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (! _channels[ch].active) continue; + if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; hasData = true; uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; uint8_t chMask = (1 << ch); - uint8_t* p = dest + written; + uint8_t* p = dest + pos; for (int bit = 7; bit >= 0; bit--) { uint8_t dataVal = (srcByte >> bit) & 1; - // Step 0: Always HIGH (start of bit) - p[0] |= chMask; + // Half-word swapped: memory layout [step2, step3, step0, step1] + // Step 0 (HIGH) -> p[2] + p[2] |= chMask; - // Step 1: HIGH for '1', LOW for '0' - if (dataVal) p[1] |= chMask; + // Step 1 (data) -> p[3] + if (dataVal) p[3] |= chMask; - // Step 2: HIGH for '1', LOW for '0' - if (dataVal) p[2] |= chMask; + // Step 2 (data) -> p[0] + if (dataVal) p[0] |= chMask; - // Step 3: Always LOW (already 0) + // Step 3 (LOW) -> p[1] (already 0) p += 4; } @@ -665,16 +752,14 @@ void I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, size_t* bytesWrit } } - written += 32; + pos += 32; } - - *bytesWritten = written; + // Rest of buffer remains zero (reset signal) from memset } -void I2sBusContext:: fillBuffer(uint8_t bufIdx) { - size_t written; - encode4Step(_dmaBuffer[bufIdx], _bufferSize, &written); - _dmaDesc[bufIdx]->length = written; +void I2sBusContext::fillBuffer(uint8_t bufIdx) { + encode4Step(_dmaBuffer[bufIdx], _bufferSize); + // desc->length stays at _bufferSize (set in init, never changes) } bool I2sBusContext::startTransmit() { @@ -691,51 +776,51 @@ bool I2sBusContext::startTransmit() { } } + static uint32_t s_txCount = 0; + if (s_txCount < 3 || (s_txCount % 1000) == 0) { + Serial.printf("[I2S] startTransmit #%u: channels=%u, maxDataLen=%u, bufSize=%u\n", + s_txCount, _channelCount, _maxDataLen, _bufferSize); + } + s_txCount++; + // Fill both buffers initially fillBuffer(0); fillBuffer(1); - // Check if we need second buffer - bool moreData = false; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - moreData = true; - break; - } - } - - // Setup DMA chain - if (moreData) { - _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; - _dmaDesc[1]->qe.stqe_next = nullptr; // Will be set in ISR if more data - } else { - _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]->length > 0 ? _dmaDesc[1] : nullptr; - _dmaDesc[1]->qe.stqe_next = nullptr; - } + // Restore DMA descriptor ownership (DMA clears owner to 0 after processing) + _dmaDesc[0]->owner = 1; + _dmaDesc[1]->owner = 1; _activeBuffer = 0; _state = DriverState::Sending; - // Reset FIFO - _i2sDev->conf.tx_fifo_reset = 1; - _i2sDev->conf.tx_fifo_reset = 0; + // Reset DMA and FIFO before starting + _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; - // Clear interrupts + // Clear and enable interrupts _i2sDev->int_clr.val = 0xFFFFFFFF; - - // Enable EOF interrupt _i2sDev->int_ena.out_eof = 1; - // Set DMA start address + // Enable DMA and start + _i2sDev->fifo_conf.dscr_en = 1; + _i2sDev->out_link.start = 0; _i2sDev->out_link.addr = (uint32_t)_dmaDesc[0]; - - // Start transmission _i2sDev->out_link.start = 1; _i2sDev->conf.tx_start = 1; return true; } +static volatile uint32_t s_i2sIsrCount = 0; +static volatile uint32_t s_i2sIsrSending = 0; +static volatile uint32_t s_i2sIsrReset = 0; +static volatile uint32_t s_i2sIsrIdle = 0; + void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { I2sBusContext* ctx = (I2sBusContext*)arg; i2s_dev_t* dev = ctx->_i2sDev; @@ -743,36 +828,43 @@ void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { uint32_t status = dev->int_st.val; dev->int_clr.val = status; - if (status & I2S_OUT_EOF_INT_ST) { - // Current buffer finished, check if more data + if (!(status & I2S_OUT_EOF_INT_ST)) return; + + s_i2sIsrCount++; + + // The completed buffer just finished playing; DMA is now on the other buffer + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; + + if (ctx->_state == DriverState::Sending) { + s_i2sIsrSending++; + // Encode next chunk into the completed buffer + // encode4Step always fills the full buffer (zeros for any remainder) + ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); + + // Check if all source data has been consumed bool moreData = false; for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && + if (ctx->_channels[ch].active && ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { moreData = true; break; } } - - if (moreData) { - // Refill the completed buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer ^= 1; // Switch to other buffer - - // Fill the completed buffer with new data - size_t written; - ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize, &written); - ctx->_dmaDesc[completedBuf]->length = written; - - // Link it after current buffer - ctx->_dmaDesc[ctx->_activeBuffer]->qe.stqe_next = ctx->_dmaDesc[completedBuf]; - ctx->_dmaDesc[completedBuf]->qe.stqe_next = nullptr; - } else { - // All data sent, add reset time then go idle - ctx->_state = DriverState::Idle; - dev->conf.tx_start = 0; - dev->out_link.start = 0; + if (!moreData) { + s_i2sIsrReset++; + ctx->_state = DriverState::WaitingReset; } + + // Restore DMA ownership so hardware can replay this buffer + ctx->_dmaDesc[completedBuf]->owner = 1; + } else { + // WaitingReset - one full zero buffer has been sent as reset, stop DMA + s_i2sIsrIdle++; + dev->int_ena.out_eof = 0; + dev->conf.tx_start = 0; + dev->out_link.start = 0; + ctx->_state = DriverState::Idle; } } @@ -790,7 +882,6 @@ I2sBus::I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, , _ctx(nullptr) , _encodeBuffer(nullptr) , _encodeBufferSize(0) - , _encodedLen(0) { } @@ -812,12 +903,14 @@ bool I2sBus::begin() { _channelIdx = _ctx->registerChannel(_pin, this); if (_channelIdx < 0) { + Serial.printf("[I2S] registerChannel failed for pin %d\n", _pin); I2sBusContext::release(_busNum); _ctx = nullptr; return false; } _initialized = true; + Serial.printf("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n", _pin, _busNum, _channelIdx); return true; } @@ -825,13 +918,15 @@ void I2sBus::end() { if (!_initialized) return; if (_ctx) { + // Wait for any active transmission to complete before cleanup + while (!_ctx->isIdle()) vTaskDelay(1); _ctx->unregisterChannel(_channelIdx); I2sBusContext::release(_busNum); _ctx = nullptr; } if (_encodeBuffer) { - free(_encodeBuffer); + heap_caps_free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } @@ -839,17 +934,17 @@ void I2sBus::end() { _initialized = false; } -bool I2sBus:: allocateBuffer(uint16_t numPixels) { +bool I2sBus::allocateBuffer(uint16_t numPixels) { size_t needed = numPixels * getChannelCount(_order); if (_encodeBuffer && _encodeBufferSize >= needed) { return true; } if (_encodeBuffer) { - free(_encodeBuffer); + heap_caps_free(_encodeBuffer); } - _encodeBuffer = (uint8_t*)malloc(needed); + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); if (!_encodeBuffer) { _encodeBufferSize = 0; return false; @@ -859,31 +954,44 @@ bool I2sBus:: allocateBuffer(uint16_t numPixels) { } bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!_initialized || !_ctx || !pixels || numPixels == 0) return false; + // Always use internal pixel buffer (WLED always calls show() without args) + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - // Wait for previous transmission - while (! _ctx->isIdle()) { + // Wait for previous transmission to complete + while (!_ctx->isIdle()) { vTaskDelay(1); } - if (!allocateBuffer(numPixels)) return false; + if (!allocateBuffer(_numPixels)) return false; - // Encode pixels + // Encode pixels to byte stream ColorEncoder encoder(_order); uint8_t* dst = _encodeBuffer; uint8_t numCh = encoder.getNumChannels(); - for (uint16_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); dst += numCh; } - _encodedLen = numPixels * numCh; - - // Set data for our channel - _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); + // Debug: print ISR stats periodically + static uint32_t s_showCount = 0; + extern volatile uint32_t s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle; + if (s_showCount < 3 || (s_showCount % 1000) == 0) { + Serial.printf("[I2S] show #%u: %u px, %uch, %u bytes | ISR: total=%u send=%u reset=%u idle=%u\n", + s_showCount, _numPixels, numCh, _numPixels * numCh, + s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle); + // Print first 16 bytes of encode buffer + Serial.printf("[I2S] Encoded[0..15]: "); + for (int i = 0; i < 16 && i < (int)(_numPixels * numCh); i++) { + Serial.printf("%02X ", _encodeBuffer[i]); + } + Serial.println(); + } + s_showCount++; - // Start transmission + // Set data for our channel and start + _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); return _ctx->startTransmit(); } @@ -1636,6 +1744,448 @@ void LcdBus::setColorOrder(ColorOrder order) { #endif // WLEDPB_LCD_SUPPORT +//============================================================================== +// SPI Parallel Bus Implementation (ESP32-C3) +//============================================================================== + +#ifdef WLEDPB_SPI_SUPPORT + +// Pin assignments for SPI2 quad mode on C3 +// SPI2 signals: FSPID (MOSI/D0), FSPIQ (MISO/D1), FSPIWP (D2), FSPIHD (D3) +static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_OUT_IDX, FSPIHD_OUT_IDX }; + +// Encoding patterns for SPI quad mode (4-step cadence, LSB first) +// Each lane is one bit position in a nibble, one byte = two clock cycles, 2 bytes = one 4-step bit +static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high +static constexpr uint16_t SPI_ONE_BIT = 0x0011; // output: [1,1,0,0] = 50% high + +SpiBusContext* SpiBusContext::_instance = nullptr; +uint8_t SpiBusContext::_refCount = 0; + +SpiBusContext* SpiBusContext::get() { + if (_instance == nullptr) { + _instance = new SpiBusContext(); + } + _refCount++; + return _instance; +} + +void SpiBusContext::release() { + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } +} + +SpiBusContext::SpiBusContext() + : _sending(false) + , _initialized(false) + , _currentBuffer(0) + , _isrHandle(nullptr) + , _hw(&GPSPI2) + , _channelCount(0) + , _framePos(0) + , _numBytes(0) +{ + _dmaBuffer[0] = _dmaBuffer[1] = nullptr; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, false}; + } +} + +SpiBusContext::~SpiBusContext() { + deinit(); +} + +void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { + uint8_t* dst = _dmaBuffer[bufIdx]; + memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); + + if (!_sending) return; + + size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte + size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; + size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; + + if (srcThisChunk == 0) { + _sending = false; + _framePos = 0; + return; + } + + for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { + if (!_channels[lane].active || !_channels[lane].srcData) continue; + + const uint16_t zerobit = SPI_ZERO_BIT << lane; + const uint16_t onebit = SPI_ONE_BIT << lane; + const uint8_t* src = _channels[lane].srcData; + size_t srcLen = _channels[lane].srcLen; + uint16_t* pOut = reinterpret_cast(dst); + + for (size_t i = 0; i < srcThisChunk; i++) { + size_t srcIdx = _framePos + i; + uint8_t v = (srcIdx < srcLen) ? src[srcIdx] : 0; + *pOut++ |= (v & 0x80) ? onebit : zerobit; + *pOut++ |= (v & 0x40) ? onebit : zerobit; + *pOut++ |= (v & 0x20) ? onebit : zerobit; + *pOut++ |= (v & 0x10) ? onebit : zerobit; + *pOut++ |= (v & 0x08) ? onebit : zerobit; + *pOut++ |= (v & 0x04) ? onebit : zerobit; + *pOut++ |= (v & 0x02) ? onebit : zerobit; + *pOut++ |= (v & 0x01) ? onebit : zerobit; + } + } + _framePos += srcThisChunk; +} + +void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { + SpiBusContext* ctx = (SpiBusContext*)arg; + gdma_dev_t* dma = &GDMA; + + if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + if (ctx->_currentBuffer == 0) { + ctx->encodeSpiChunk(0); + ctx->_currentBuffer = 1; + } else { + ctx->encodeSpiChunk(1); + ctx->_currentBuffer = 0; + } + } + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; +} + +bool SpiBusContext::init(const LedTiming& timing) { + if (_initialized) return true; + + Serial.printf("[SPI] Initializing SPI parallel bus context\n"); + + // Allocate DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, + MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (!_dmaBuffer[i]) { + Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, WLEDPB_SPI_DMA_BUFFER_SIZE); + } + + // Setup DMA descriptors - circular linked list + for (int i = 0; i < 2; i++) { + _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].owner = 1; + _dmaDesc[i].sosf = 0; + _dmaDesc[i].eof = 1; + _dmaDesc[i].buf = _dmaBuffer[i]; + } + _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; + _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; + + // Enable peripheral clocks (SPI2 + DMA) + SYSTEM.perip_clk_en0.reg_spi2_clk_en = 1; + SYSTEM.perip_rst_en0.reg_spi2_rst = 1; + SYSTEM.perip_rst_en0.reg_spi2_rst = 0; + + SYSTEM.perip_clk_en1.reg_dma_clk_en = 1; + SYSTEM.perip_rst_en1.reg_dma_rst = 1; + SYSTEM.perip_rst_en1.reg_dma_rst = 0; + + // Configure SPI2 master + spi_ll_master_init(_hw); + spi_ll_master_set_mode(_hw, 0); + spi_ll_set_tx_lsbfirst(_hw, true); + + spi_line_mode_t linemode = {}; + linemode.data_lines = 4; // quad mode + spi_ll_master_set_line_mode(_hw, linemode); + + // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config + // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) + uint32_t bitPeriodNs = timing.bitPeriod(); + // Calculate SPI clock from timing: each DMA byte = 2 clock cycles (2 nibbles), + // each source bit = 2 bytes = 4 clock cycles → clock freq = 4 / (bitPeriod_ns * 1e-9) + uint32_t targetFreq = 4000000000UL / bitPeriodNs; + if (targetFreq < 2000000) targetFreq = 2000000; + if (targetFreq > 5000000) targetFreq = 5000000; + + spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); + + Serial.printf("[SPI] Bit period: %uns, target SPI freq: %uHz\n", bitPeriodNs, targetFreq); + + // Route SPI clock to a dummy pin (needed for DMA to work) + // GPIO11 is VDD_SPI on C3 but routing CLK to it doesn't affect power + pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + + spi_ll_enable_mosi(_hw, true); + spi_ll_dma_tx_enable(_hw, true); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + // Configure GDMA + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + + // Enable EOF interrupt + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + // Install ISR + esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, + ESP_INTR_FLAG_IRAM, + gdmaISR, this, &_isrHandle); + if (err != ESP_OK) { + Serial.printf("[SPI] ISR alloc failed: %d\n", err); + deinit(); + return false; + } + + _initialized = true; + Serial.printf("[SPI] Init complete\n"); + return true; +} + +void SpiBusContext::deinit() { + _sending = false; + + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + } + + SYSTEM.perip_rst_en0.reg_spi2_rst = 1; + _initialized = false; +} + +int8_t SpiBusContext::registerChannel(int8_t pin, SpiBus* bus) { + int8_t idx = -1; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + + // Route SPI data signal to GPIO + pinMode(pin, OUTPUT); + pinMatrixOutAttach(pin, SPI_SIGNAL_INDICES[idx], false, false); + + Serial.printf("[SPI] Registered channel %d on pin %d (signal=%d)\n", idx, pin, SPI_SIGNAL_INDICES[idx]); + return idx; +} + +void SpiBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, false}; + _channelCount--; +} + +void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + if (len > _numBytes) _numBytes = len; +} + +void SpiBusContext::resetAndStart() { + // Reset SPI + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + // Reset GDMA + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + + // Start SPI transfer + spi_ll_user_start(_hw); +} + +bool SpiBusContext::startTransmit() { + if (_sending) return false; + if (_channelCount == 0) return false; + + _framePos = 0; + _numBytes = 0; + for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcLen > _numBytes) { + _numBytes = _channels[ch].srcLen; + } + } + + // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte + // Plus reset: extra zero bits at the end + uint32_t resetBits = 5000; // ~300us reset at ~2.6MHz + uint32_t totalBits = (_numBytes * 16 * 8) + resetBits; + if (totalBits > 262143) totalBits = 262143; // SPI max 18 bits for length register + + spi_ll_set_mosi_bitlen(_hw, totalBits); + + _sending = true; + _currentBuffer = 0; + encodeSpiChunk(0); + encodeSpiChunk(1); + resetAndStart(); + + static uint32_t s_spiTxCount = 0; + if (s_spiTxCount < 3 || (s_spiTxCount % 1000) == 0) { + Serial.printf("[SPI] startTransmit #%u: %u bytes, %u totalBits\n", + s_spiTxCount, _numBytes, totalBits); + } + s_spiTxCount++; + + return true; +} + +// SpiBus implementation + +SpiBus::SpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) + : _pin(pin) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) +{ +} + +SpiBus::~SpiBus() { + end(); +} + +bool SpiBus::begin() { + if (_initialized) return true; + + _ctx = SpiBusContext::get(); + if (!_ctx) return false; + + if (!_ctx->init(_timing)) { + SpiBusContext::release(); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); + SpiBusContext::release(); + _ctx = nullptr; + return false; + } + + _initialized = true; + Serial.printf("[SPI] SpiBus::begin() OK: pin=%d, channel=%d\n", _pin, _channelIdx); + return true; +} + +void SpiBus::end() { + if (!_initialized) return; + + if (_ctx) { + while (!_ctx->isIdle()) vTaskDelay(1); + _ctx->unregisterChannel(_channelIdx); + SpiBusContext::release(); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool SpiBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + + if (_encodeBuffer) heap_caps_free(_encodeBuffer); + + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool SpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + + // Wait for previous transmission + while (!_ctx->isIdle()) { + vTaskDelay(1); + } + + // Wait for SPI to finish any remaining transfer + while (!_ctx->isSpiDone()) { + vTaskDelay(1); + } + + if (!allocateBuffer(_numPixels)) return false; + + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } + + _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); + return _ctx->startTransmit(); +} + +bool SpiBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void SpiBus::waitComplete() { + while (_ctx && !_ctx->isIdle()) { + vTaskDelay(1); + } +} + +void SpiBus::setColorOrder(ColorOrder order) { + _order = order; +} + +#endif // WLEDPB_SPI_SUPPORT + //============================================================================== // Bus Factory Implementation //============================================================================== @@ -1658,7 +2208,7 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, #ifdef WLEDPB_I2S_SUPPORT case BusType::I2S: - bus = new I2sBus(pin, timing, order, 0, bufferSize); + bus = new I2sBus(pin, timing, order, 1, bufferSize); break; #endif @@ -1668,6 +2218,12 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, break; #endif +#ifdef WLEDPB_SPI_SUPPORT + case BusType::SPI: + bus = new SpiBus(pin, timing, order); + break; +#endif + default: return nullptr; } @@ -1680,6 +2236,8 @@ BusType getRecommendedBusType() { return BusType::LCD; // S3 has best LCD support #elif defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) return BusType::I2S; // Original and S2 have I2S parallel +#elif defined(WLEDPB_SPI_SUPPORT) + return BusType::SPI; // C3 uses SPI quad mode #else return BusType::RMT; // Fallback to RMT #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 3039fb1902..37ac32aa9a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -52,6 +52,11 @@ by @dedehai, 2026 #define WLEDPB_LCD_SUPPORT #endif +// SPI parallel support (C3 - uses SPI quad mode with GDMA) +#if defined(WLEDPB_ESP32C3) + #define WLEDPB_SPI_SUPPORT +#endif + #include "WLEDpixelBus_Timings.h" namespace WLEDpixelBus { @@ -429,7 +434,7 @@ class I2sBusContext { size_t _maxDataLen; // Encoding (4-step cadence) - void encode4Step(uint8_t* dest, size_t destLen, size_t* bytesWritten); + void encode4Step(uint8_t* dest, size_t destLen); // Singleton instances static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; @@ -450,7 +455,7 @@ class I2sBus : public IBus { * @param bufferSize DMA buffer size */ I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, - uint8_t busNum = 0, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + uint8_t busNum = 1, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); ~I2sBus() override; bool begin() override; @@ -465,10 +470,6 @@ class I2sBus : public IBus { void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(ColorOrder order); - // Access for context - const uint8_t* getEncodedData() const { return _encodeBuffer; } - size_t getEncodedLen() const { return _encodedLen; } - private: int8_t _pin; uint8_t _busNum; @@ -482,7 +483,6 @@ class I2sBus : public IBus { uint8_t* _encodeBuffer; size_t _encodeBufferSize; - size_t _encodedLen; bool allocateBuffer(uint16_t numPixels); }; @@ -635,6 +635,118 @@ class LcdBus : public IBus { #endif // WLEDPB_LCD_SUPPORT +//============================================================================== +// SPI Parallel Bus - ESP32-C3 (uses SPI2 quad mode + GDMA) +//============================================================================== +#ifdef WLEDPB_SPI_SUPPORT + +#include "hal/spi_ll.h" +#include "soc/gdma_struct.h" +#include "hal/gdma_ll.h" +#include "soc/gdma_reg.h" +#include "rom/lldesc.h" + +#define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines +#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 +#define WLEDPB_SPI_GDMA_CHANNEL 0 + +/** + * SPI bus context - manages SPI2 quad mode for parallel LED output on C3 + * Uses GDMA with circular linked-list and ISR-driven buffer refill + */ +class SpiBusContext { +public: + static SpiBusContext* get(); + static void release(); + + bool init(const LedTiming& timing); + void deinit(); + + int8_t registerChannel(int8_t pin, SpiBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + bool startTransmit(); + bool isIdle() const { return !_sending; } + bool isSpiDone() const { return _hw ? spi_ll_usr_is_done(_hw) : true; } + + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + SpiBusContext(); + ~SpiBusContext(); + + void encodeSpiChunk(uint8_t bufIdx); + void resetAndStart(); + + static void IRAM_ATTR gdmaISR(void* arg); + + volatile bool _sending; + bool _initialized; + volatile uint8_t _currentBuffer; + + // DMA + uint8_t* _dmaBuffer[2]; + lldesc_t _dmaDesc[2]; + intr_handle_t _isrHandle; + + // SPI device + spi_dev_t* _hw; + + // Source data per channel + struct ChannelData { + SpiBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + bool active; + }; + ChannelData _channels[WLEDPB_SPI_MAX_CHANNELS]; + uint8_t _channelCount; + size_t _framePos; // current source byte position + size_t _numBytes; // total source bytes to send + + static SpiBusContext* _instance; + static uint8_t _refCount; +}; + +/** + * SPI parallel output bus (for ESP32-C3) + */ +class SpiBus : public IBus { +public: + SpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~SpiBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "SPI"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); + +private: + bool allocateBuffer(uint16_t numPixels); + + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + int8_t _channelIdx; + SpiBusContext* _ctx; + + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; +}; + +#endif // WLEDPB_SPI_SUPPORT + //============================================================================== // Bus Factory - Create appropriate bus for platform //============================================================================== @@ -643,6 +755,7 @@ enum class BusType : uint8_t { RMT = 0, I2S = 1, LCD = 2, + SPI = 3, Auto = 255 }; From df9b94e361e071316659ba69df0be64260a3d2f5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 16:33:02 +0100 Subject: [PATCH 010/173] revert unnecessary changes --- wled00/data/index.htm | 3 +-- wled00/data/index.js | 2 +- wled00/data/settings_leds.htm | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/wled00/data/index.htm b/wled00/data/index.htm index b878e82e18..253bc3bbd4 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -369,5 +369,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/wled00/data/index.js b/wled00/data/index.js index b1b174e31a..cd508e8642 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3465,4 +3465,4 @@ _C.addEventListener('touchstart', lock, false); _C.addEventListener('mouseout', move, false); _C.addEventListener('mouseup', move, false); -_C.addEventListener('touchend', move, false); \ No newline at end of file +_C.addEventListener('touchend', move, false); \ No newline at end of file diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index e304f087b0..91935768a9 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -1183,5 +1183,4 @@

Advanced

- - \ No newline at end of file + \ No newline at end of file From 83a9dfe9601c32097af32af1af17a5c0493532da Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 17:01:39 +0100 Subject: [PATCH 011/173] intermediate fixes --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 15 ++++++++++++--- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index dd37571b31..8637eef397 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -852,14 +852,23 @@ void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { } } if (!moreData) { - s_i2sIsrReset++; - ctx->_state = DriverState::WaitingReset; + // Last data chunk was just encoded into completedBuf. + // DMA is currently playing the OTHER buffer. We need to wait + // for that to finish so the last-data buffer gets played. + ctx->_state = DriverState::SendingLast; } // Restore DMA ownership so hardware can replay this buffer ctx->_dmaDesc[completedBuf]->owner = 1; + } else if (ctx->_state == DriverState::SendingLast) { + // The other buffer just finished. DMA is now playing the last-data buffer. + // Fill completed buffer with zeros (reset signal) so it plays after. + memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); + ctx->_dmaDesc[completedBuf]->owner = 1; + s_i2sIsrReset++; + ctx->_state = DriverState::WaitingReset; } else { - // WaitingReset - one full zero buffer has been sent as reset, stop DMA + // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. s_i2sIsrIdle++; dev->int_ena.out_eof = 0; dev->conf.tx_start = 0; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 37ac32aa9a..65ecbff7af 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -151,7 +151,8 @@ struct CctPixel { enum class DriverState : uint8_t { Idle = 0, Sending = 1, - WaitingReset = 2 + SendingLast = 2, // Last data buffer was filled; wait for current buffer to finish so last-data buffer plays + WaitingReset = 3 // Last data buffer played; zero buffer playing as reset signal }; //============================================================================== From b34f3d3f2cedd5648449ab1bbd640d4eae788db0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 17:16:22 +0100 Subject: [PATCH 012/173] fix I2S crashes --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 8 ++++---- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 8637eef397..48bad766e4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -696,7 +696,7 @@ void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ } } -void I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { // 4-step cadence encoding for parallel output // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) // Desired output: [HIGH][data][data][LOW] for each bit @@ -1368,7 +1368,7 @@ void LcdBusContext:: setChannelData(int8_t channelIdx, const uint8_t* data, size } } -size_t LcdBusContext:: encodeIntoBuffer(uint8_t* dest, size_t destLen) { +size_t IRAM_ATTR LcdBusContext:: encodeIntoBuffer(uint8_t* dest, size_t destLen) { // Encode pixel data from all channels // Returns number of bytes written (0 if no more data) @@ -1432,7 +1432,7 @@ size_t LcdBusContext:: encodeIntoBuffer(uint8_t* dest, size_t destLen) { return written; } -bool LcdBusContext::hasMoreData() const { +bool IRAM_ATTR LcdBusContext::hasMoreData() const { for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { return true; @@ -1441,7 +1441,7 @@ bool LcdBusContext::hasMoreData() const { return false; } -void LcdBusContext::fillBufferForState(uint8_t bufIdx) { +void IRAM_ATTR LcdBusContext::fillBufferForState(uint8_t bufIdx) { // Fill buffer based on current transmission state // This is called from ISR context! diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 65ecbff7af..c32b9fd6e0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -435,7 +435,7 @@ class I2sBusContext { size_t _maxDataLen; // Encoding (4-step cadence) - void encode4Step(uint8_t* dest, size_t destLen); + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); // Singleton instances static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; @@ -554,9 +554,9 @@ class LcdBusContext { LcdBusContext(); ~LcdBusContext(); - size_t encodeIntoBuffer(uint8_t* dest, size_t destLen); - bool hasMoreData() const; - void fillBufferForState(uint8_t bufIdx); + size_t IRAM_ATTR encodeIntoBuffer(uint8_t* dest, size_t destLen); + bool IRAM_ATTR hasMoreData() const; + void IRAM_ATTR fillBufferForState(uint8_t bufIdx); static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, From ec22caeb0b28195336a5a6ed4688cb1c84e04881 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 17:43:56 +0100 Subject: [PATCH 013/173] increase RMT priority, remove debug --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 46 ++++++++---------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 48bad766e4..f4431396eb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -169,12 +169,22 @@ bool RmtBus::begin() { return false; } - // Use interrupt flags matching NeoPixelBus behavior -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 3, 0) - err = rmt_driver_install(_rmtChannel, 0, ESP_INTR_FLAG_LOWMED); -#else - err = rmt_driver_install(_rmtChannel, 0, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1); + // Prioritize RMT over I2S/SPI DMA interrupts (which use LEVEL1) to prevent starvation. + // Try LEVEL3 first, fallback to LEVEL2, then LEVEL1. + int flags = ESP_INTR_FLAG_IRAM; +#ifdef ESP_INTR_FLAG_LEVEL3 + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL3); + if (err != ESP_OK) +#endif + { +#ifdef ESP_INTR_FLAG_LEVEL2 + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL2); + if (err != ESP_OK) #endif + { + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL1); + } + } if (err != ESP_OK) { return false; } @@ -225,7 +235,6 @@ bool RmtBus::allocateBuffer(uint16_t numPixels) { } bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - static uint32_t showDbgCnt = 0; if (!pixels) { pixels = _pixelData; numPixels = _numPixels; @@ -266,9 +275,7 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc // Start transmission size_t dataLen = numPixels * numCh; - if (showDbgCnt++ < 20) Serial.printf("[WPB] show() ch=%d numPix=%u numCh=%u dataLen=%u pix[0]=0x%08X\n", _rmtChannel, numPixels, numCh, dataLen, pixels[0]); esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); - if (showDbgCnt <= 3 && err != ESP_OK) Serial.printf("[WPB] rmt_write_sample FAIL: %d (%s)\n", err, esp_err_to_name(err)); return err == ESP_OK; } @@ -776,13 +783,6 @@ bool I2sBusContext::startTransmit() { } } - static uint32_t s_txCount = 0; - if (s_txCount < 3 || (s_txCount % 1000) == 0) { - Serial.printf("[I2S] startTransmit #%u: channels=%u, maxDataLen=%u, bufSize=%u\n", - s_txCount, _channelCount, _maxDataLen, _bufferSize); - } - s_txCount++; - // Fill both buffers initially fillBuffer(0); fillBuffer(1); @@ -983,22 +983,6 @@ bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc dst += numCh; } - // Debug: print ISR stats periodically - static uint32_t s_showCount = 0; - extern volatile uint32_t s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle; - if (s_showCount < 3 || (s_showCount % 1000) == 0) { - Serial.printf("[I2S] show #%u: %u px, %uch, %u bytes | ISR: total=%u send=%u reset=%u idle=%u\n", - s_showCount, _numPixels, numCh, _numPixels * numCh, - s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle); - // Print first 16 bytes of encode buffer - Serial.printf("[I2S] Encoded[0..15]: "); - for (int i = 0; i < 16 && i < (int)(_numPixels * numCh); i++) { - Serial.printf("%02X ", _encodeBuffer[i]); - } - Serial.println(); - } - s_showCount++; - // Set data for our channel and start _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); return _ctx->startTransmit(); From 6b8cfaed3ae3f464b4af7ae029385a44fad0c09b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 18:05:45 +0100 Subject: [PATCH 014/173] fix RMT parallel sending --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index f4431396eb..0add0adac4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -247,14 +247,8 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc return false; } - // Wait for ALL active RMT channels to complete their previous transmission. - // This ensures frame-level synchronization when multiple RMT outputs are used - // (prevents one channel from starting a new frame while another is still sending). - for (uint8_t ch = 0; ch < getRmtMaxChannels(); ch++) { - if (s_activeChannelMask & (1 << ch)) { - rmt_wait_tx_done((rmt_channel_t)ch, portMAX_DELAY); - } - } + // Wait for previous transmission on THIS channel to complete + rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); if (!allocateBuffer(numPixels)) return false; From 7133dd31618e88fd259d7fe3b3899f790e292e42 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 18:17:34 +0100 Subject: [PATCH 015/173] fix sequential I2S send --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 23 +++++++++++++++++++++++ wled00/src/WLEDpixelBus/WLEDpixelBus.h | 2 ++ 2 files changed, 25 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 0add0adac4..7b74990ba0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -391,6 +391,7 @@ I2sBusContext::I2sBusContext(uint8_t busNum) , _isrHandle(nullptr) , _channelCount(0) , _channelMask(0) + , _stagedMask(0) , _maxDataLen(0) { #if defined(WLEDPB_ESP32) @@ -695,6 +696,12 @@ void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ if (len > _maxDataLen) { _maxDataLen = len; } + + // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() + if (_stagedMask & (1 << channelIdx)) { + _stagedMask = 0; + } + _stagedMask |= (1 << channelIdx); } void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { @@ -767,6 +774,10 @@ 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 false; + _stagedMask = 0; // Reset for next frame + _maxDataLen = 0; for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { if (_channels[ch].active) { @@ -1085,6 +1096,7 @@ LcdBusContext::LcdBusContext() , _timing{0, 0, 0, 0, 0} , _channelCount(0) , _channelMask(0) + , _stagedMask(0) , _maxDataLen(0) , _activeBuffer(0) , _resetBytesRemaining(0) @@ -1344,6 +1356,11 @@ void LcdBusContext:: setChannelData(int8_t channelIdx, const uint8_t* data, size if (len > _maxDataLen) { _maxDataLen = len; } + + if (_stagedMask & (1 << channelIdx)) { + _stagedMask = 0; + } + _stagedMask |= (1 << channelIdx); } size_t IRAM_ATTR LcdBusContext:: encodeIntoBuffer(uint8_t* dest, size_t destLen) { @@ -1473,6 +1490,12 @@ void IRAM_ATTR LcdBusContext::fillBufferForState(uint8_t bufIdx) { } bool LcdBusContext::startTransmit() { + if (_stagedMask != _channelMask) { + return false; // wait for all channels (from multiple buses) + } + + _stagedMask = 0; // ready for next frame once we transmit + if (_state != DriverState::Idle) { LCD_LOG("ERROR: Not idle"); return false; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index c32b9fd6e0..b9924f4917 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -432,6 +432,7 @@ class I2sBusContext { ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; uint8_t _channelCount; uint16_t _channelMask; + uint16_t _stagedMask; size_t _maxDataLen; // Encoding (4-step cadence) @@ -590,6 +591,7 @@ class LcdBusContext { ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; uint8_t _channelCount; uint8_t _channelMask; + uint8_t _stagedMask; size_t _maxDataLen; static LcdBusContext* _instance; From 2a4c9b1d783ead341c462948534df96e1a623f13 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 10 Mar 2026 23:49:50 +0100 Subject: [PATCH 016/173] fix compile errors for C3 SPI driver, enable driver selection for C3 still not working --- wled00/bus_wrapper.h | 4 ++ wled00/const.h | 2 +- wled00/data/settings_leds.htm | 4 +- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 61 ++++++++++++++---------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 28 ++++++----- 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 0f6dac3c28..70b5e53f81 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -195,6 +195,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, btype = WLEDpixelBus::BusType::LCD; #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) btype = WLEDpixelBus::BusType::I2S; + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + btype = WLEDpixelBus::BusType::SPI; #else btype = WLEDpixelBus::BusType::RMT; #endif @@ -245,6 +247,8 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, btype = WLEDpixelBus::BusType::LCD; #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) btype = WLEDpixelBus::BusType::I2S; + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + btype = WLEDpixelBus::BusType::SPI; #else btype = WLEDpixelBus::BusType::RMT; #endif diff --git a/wled00/const.h b/wled00/const.h index a681baa744..7a6f736cf1 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -67,7 +67,7 @@ constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_C #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB + #define WLED_MAX_I2S_CHANNELS 4 // SPI parallel output supported by WLEDpixelBus //#define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 91935768a9..727500789e 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -450,8 +450,8 @@ s.style.color = "#fff"; // reset if (isDig(t) && !isD2P(t)) { - // Update I2S/RMT driver info/dropdown for ESP32 digital buses, C3 only supports RMT - if (!is8266() && !isC3()) { + // Update I2S/RMT driver info/dropdown for ESP32 digital buses + if (!is8266()) { // Show driver selection dropdown when I2S is enabled, mark red if invalid if (drvsel) { if (d.Sf.AS.checked) drvsel.style.display = "inline"; // only show when advanced settings enabled diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 7b74990ba0..798977a247 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -16,6 +16,15 @@ by @dedehai, 2026 #include "WLEDpixelBus.h" +#ifdef WLEDPB_SPI_SUPPORT +#undef FLAG_ATTR +#define FLAG_ATTR(TYPE) +#include "hal/spi_ll.h" +#include "driver/periph_ctrl.h" +#include "esp_private/gdma.h" +#include "esp_rom_gpio.h" +#endif + namespace WLEDpixelBus { //============================================================================== @@ -1025,7 +1034,7 @@ Key design: #ifdef WLEDPB_LCD_SUPPORT -#include "driver/periph_ctrl.h" +#include "esp_private/periph_ctrl.h" #include "esp_private/gdma.h" #include "esp_rom_gpio.h" #include "hal/dma_types.h" @@ -1809,6 +1818,10 @@ SpiBusContext::~SpiBusContext() { deinit(); } +bool SpiBusContext::isSpiDone() const { + return _hw ? spi_ll_usr_is_done(_hw) : true; +} + void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { uint8_t* dst = _dmaBuffer[bufIdx]; memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); @@ -1896,13 +1909,8 @@ bool SpiBusContext::init(const LedTiming& timing) { _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; // Enable peripheral clocks (SPI2 + DMA) - SYSTEM.perip_clk_en0.reg_spi2_clk_en = 1; - SYSTEM.perip_rst_en0.reg_spi2_rst = 1; - SYSTEM.perip_rst_en0.reg_spi2_rst = 0; - - SYSTEM.perip_clk_en1.reg_dma_clk_en = 1; - SYSTEM.perip_rst_en1.reg_dma_rst = 1; - SYSTEM.perip_rst_en1.reg_dma_rst = 0; + periph_module_enable(PERIPH_SPI2_MODULE); + periph_module_enable(PERIPH_GDMA_MODULE); // Configure SPI2 master spi_ll_master_init(_hw); @@ -1934,7 +1942,8 @@ bool SpiBusContext::init(const LedTiming& timing) { 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); + _hw->cmd.update = 1; + while (_hw->cmd.update); // Configure GDMA gdma_dev_t* dma = &GDMA; @@ -1975,11 +1984,11 @@ void SpiBusContext::deinit() { } } - SYSTEM.perip_rst_en0.reg_spi2_rst = 1; + periph_module_disable(PERIPH_SPI2_MODULE); _initialized = false; } -int8_t SpiBusContext::registerChannel(int8_t pin, SpiBus* bus) { +int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus) { int8_t idx = -1; for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { if (!_channels[i].active) { @@ -2025,7 +2034,8 @@ void SpiBusContext::resetAndStart() { // Reset SPI spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); - spi_ll_apply_config(_hw); + _hw->cmd.update = 1; + while (_hw->cmd.update); // Reset GDMA gdma_dev_t* dma = &GDMA; @@ -2035,7 +2045,7 @@ void SpiBusContext::resetAndStart() { gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); // Start SPI transfer - spi_ll_user_start(_hw); + _hw->cmd.usr = 1; } bool SpiBusContext::startTransmit() { @@ -2076,7 +2086,7 @@ bool SpiBusContext::startTransmit() { // SpiBus implementation -SpiBus::SpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) +ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) : _pin(pin) , _timing(timing) , _order(order) @@ -2088,11 +2098,11 @@ SpiBus::SpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) { } -SpiBus::~SpiBus() { +ParallelSpiBus::~ParallelSpiBus() { end(); } -bool SpiBus::begin() { +bool ParallelSpiBus::begin() { if (_initialized) return true; _ctx = SpiBusContext::get(); @@ -2113,11 +2123,11 @@ bool SpiBus::begin() { } _initialized = true; - Serial.printf("[SPI] SpiBus::begin() OK: pin=%d, channel=%d\n", _pin, _channelIdx); + Serial.printf("[SPI] ParallelSpiBus::begin() OK: pin=%d, channel=%d\n", _pin, _channelIdx); return true; } -void SpiBus::end() { +void ParallelSpiBus::end() { if (!_initialized) return; if (_ctx) { @@ -2136,7 +2146,7 @@ void SpiBus::end() { _initialized = false; } -bool SpiBus::allocateBuffer(uint16_t numPixels) { +bool ParallelSpiBus::allocateBuffer(uint16_t numPixels) { size_t needed = numPixels * getChannelCount(_order); if (_encodeBuffer && _encodeBufferSize >= needed) return true; @@ -2151,7 +2161,7 @@ bool SpiBus::allocateBuffer(uint16_t numPixels) { return true; } -bool SpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { +bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; // Wait for previous transmission @@ -2179,18 +2189,18 @@ bool SpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc return _ctx->startTransmit(); } -bool SpiBus::canShow() const { +bool ParallelSpiBus::canShow() const { if (!_ctx) return true; return _ctx->isIdle(); } -void SpiBus::waitComplete() { +void ParallelSpiBus::waitComplete() { while (_ctx && !_ctx->isIdle()) { vTaskDelay(1); } } -void SpiBus::setColorOrder(ColorOrder order) { +void ParallelSpiBus::setColorOrder(ColorOrder order) { _order = order; } @@ -2230,7 +2240,7 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, #ifdef WLEDPB_SPI_SUPPORT case BusType::SPI: - bus = new SpiBus(pin, timing, order); + bus = new ParallelSpiBus(pin, timing, order); break; #endif @@ -2253,4 +2263,5 @@ BusType getRecommendedBusType() { #endif } -} // namespace WLEDpixelBus \ No newline at end of file +} // namespace WLEDpixelBus + diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index b9924f4917..1ae1127b0a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -57,6 +57,14 @@ by @dedehai, 2026 #define WLEDPB_SPI_SUPPORT #endif +#ifdef WLEDPB_SPI_SUPPORT +#include "soc/spi_struct.h" +#include "soc/gdma_struct.h" +#include "hal/gdma_ll.h" +#include "soc/gdma_reg.h" +#include "rom/lldesc.h" +#endif + #include "WLEDpixelBus_Timings.h" namespace WLEDpixelBus { @@ -643,16 +651,12 @@ class LcdBus : public IBus { //============================================================================== #ifdef WLEDPB_SPI_SUPPORT -#include "hal/spi_ll.h" -#include "soc/gdma_struct.h" -#include "hal/gdma_ll.h" -#include "soc/gdma_reg.h" -#include "rom/lldesc.h" - #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines #define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 #define WLEDPB_SPI_GDMA_CHANNEL 0 +class ParallelSpiBus; + /** * SPI bus context - manages SPI2 quad mode for parallel LED output on C3 * Uses GDMA with circular linked-list and ISR-driven buffer refill @@ -665,13 +669,13 @@ class SpiBusContext { bool init(const LedTiming& timing); void deinit(); - int8_t registerChannel(int8_t pin, SpiBus* bus); + int8_t registerChannel(int8_t pin, ParallelSpiBus* bus); void unregisterChannel(int8_t channelIdx); uint8_t getChannelCount() const { return _channelCount; } bool startTransmit(); bool isIdle() const { return !_sending; } - bool isSpiDone() const { return _hw ? spi_ll_usr_is_done(_hw) : true; } + bool isSpiDone() const; void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); @@ -698,7 +702,7 @@ class SpiBusContext { // Source data per channel struct ChannelData { - SpiBus* bus; + ParallelSpiBus* bus; int8_t pin; const uint8_t* srcData; size_t srcLen; @@ -716,10 +720,10 @@ class SpiBusContext { /** * SPI parallel output bus (for ESP32-C3) */ -class SpiBus : public IBus { +class ParallelSpiBus : public IBus { public: - SpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); - ~SpiBus() override; + ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~ParallelSpiBus() override; bool begin() override; void end() override; From 61434e7d17ec9b8b73de514656d6d3e71a6dc062 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 11 Mar 2026 19:42:24 +0100 Subject: [PATCH 017/173] minor changes to SPI output for C3, still non working, added debug --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 88 +++++++++++++++++++----- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 798977a247..e6c18acb1e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1833,6 +1833,7 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; if (srcThisChunk == 0) { + if (_sending) Serial.printf("[SPI] encodeSpiChunk: done sending frame\n"); _sending = false; _framePos = 0; return; @@ -1868,6 +1869,7 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { gdma_dev_t* dma = &GDMA; if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + // Serial.printf("I"); // Too fast for Serial, maybe just a counter Check later if (ctx->_currentBuffer == 0) { ctx->encodeSpiChunk(0); ctx->_currentBuffer = 1; @@ -1879,6 +1881,13 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; } +// low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) +static inline void spi_ll_apply_config(spi_dev_t *hw) +{ + hw->cmd.update = 1; + while (hw->cmd.update); //waiting config applied +} + bool SpiBusContext::init(const LedTiming& timing) { if (_initialized) return true; @@ -1908,9 +1917,13 @@ bool SpiBusContext::init(const LedTiming& timing) { _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; - // Enable peripheral clocks (SPI2 + DMA) + // Enable peripheral clocks and force-reset (SPI2 + DMA) + // periph_module_enable() uses ref counting and may be a no-op if the + // peripheral was already enabled. Explicit reset ensures clean state. periph_module_enable(PERIPH_SPI2_MODULE); + periph_module_reset(PERIPH_SPI2_MODULE); periph_module_enable(PERIPH_GDMA_MODULE); + periph_module_reset(PERIPH_GDMA_MODULE); // Configure SPI2 master spi_ll_master_init(_hw); @@ -1923,9 +1936,9 @@ bool SpiBusContext::init(const LedTiming& timing) { // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) + // Clock: 4 steps per bit → 4 SPI clock cycles per bit period. + // targetFreq = 4 / (bitPeriod_ns * 1e-9) = 4,000,000,000 / bitPeriod_ns uint32_t bitPeriodNs = timing.bitPeriod(); - // Calculate SPI clock from timing: each DMA byte = 2 clock cycles (2 nibbles), - // each source bit = 2 bytes = 4 clock cycles → clock freq = 4 / (bitPeriod_ns * 1e-9) uint32_t targetFreq = 4000000000UL / bitPeriodNs; if (targetFreq < 2000000) targetFreq = 2000000; if (targetFreq > 5000000) targetFreq = 5000000; @@ -1935,26 +1948,26 @@ bool SpiBusContext::init(const LedTiming& timing) { Serial.printf("[SPI] Bit period: %uns, target SPI freq: %uHz\n", bitPeriodNs, targetFreq); // Route SPI clock to a dummy pin (needed for DMA to work) - // GPIO11 is VDD_SPI on C3 but routing CLK to it doesn't affect power + // GPIO11 is VDD_SPI on C3 but routing CLK to it doesn't affect power. + // We don't set it as OUTPUT manually to allow the matrix to take full control. pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); 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); - _hw->cmd.update = 1; - while (_hw->cmd.update); + + // Force clear USR bit and apply config + spi_ll_apply_config(_hw); + + Serial.printf("[SPI] init: hw->cmd.val after reset=0x%08X\n", _hw->cmd.val); // Configure GDMA gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - // Enable EOF interrupt - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - - // Install ISR + // Install ISR first, then enable interrupt (prevents spurious fire before handler is ready) esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, ESP_INTR_FLAG_IRAM, gdmaISR, this, &_isrHandle); @@ -1964,14 +1977,28 @@ bool SpiBusContext::init(const LedTiming& timing) { return false; } + // Enable EOF interrupt (after ISR is installed) + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + _initialized = true; Serial.printf("[SPI] Init complete\n"); return true; } void SpiBusContext::deinit() { + Serial.printf("[SPI] deinit() starting\n"); _sending = false; + // Stop SPI and DMA before freeing resources + if (_hw) { + _hw->cmd.usr = 0; // Stop SPI transfer + } + + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + if (_isrHandle) { esp_intr_free(_isrHandle); _isrHandle = nullptr; @@ -1985,6 +2012,7 @@ void SpiBusContext::deinit() { } periph_module_disable(PERIPH_SPI2_MODULE); + periph_module_disable(PERIPH_GDMA_MODULE); _initialized = false; } @@ -2031,17 +2059,25 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ } void SpiBusContext::resetAndStart() { - // Reset SPI + // Reset SPI FIFO and apply config spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); - _hw->cmd.update = 1; - while (_hw->cmd.update); + spi_ll_apply_config(_hw); + + // Restore DMA descriptor ownership (DMA clears owner after processing) + _dmaDesc[0].owner = 1; + _dmaDesc[1].owner = 1; - // Reset GDMA + // Reset and restart GDMA gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + + // Re-enable EOF interrupt (channel reset may clear interrupt config) + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); // Start SPI transfer @@ -2072,6 +2108,8 @@ bool SpiBusContext::startTransmit() { _currentBuffer = 0; encodeSpiChunk(0); encodeSpiChunk(1); + + Serial.printf("[SPI] startTransmit: frame size %u, total bits %u, resetting and starting\n", _numBytes, totalBits); resetAndStart(); static uint32_t s_spiTxCount = 0; @@ -2128,10 +2166,18 @@ bool ParallelSpiBus::begin() { } void ParallelSpiBus::end() { + Serial.printf("[SPI] ParallelSpiBus::end() pin=%d\n", _pin); if (!_initialized) return; if (_ctx) { - while (!_ctx->isIdle()) vTaskDelay(1); + uint32_t startWait = millis(); + while (!_ctx->isIdle()) { + if (millis() - startWait > 500) { + Serial.printf("[SPI] end() timeout waiting for idle, forcing...\n"); + break; + } + vTaskDelay(1); + } _ctx->unregisterChannel(_channelIdx); SpiBusContext::release(); _ctx = nullptr; @@ -2165,12 +2211,22 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; // Wait for previous transmission + uint32_t startWait = millis(); while (!_ctx->isIdle()) { + if (millis() - startWait > 500) { + Serial.printf("[SPI] show() timeout waiting for idle\n"); + return false; + } vTaskDelay(1); } // Wait for SPI to finish any remaining transfer + startWait = millis(); while (!_ctx->isSpiDone()) { + if (millis() - startWait > 500) { + Serial.printf("[SPI] show() timeout waiting for SpiDone\n"); + return false; + } vTaskDelay(1); } From cd0225f19fa71327902478862e22ac36dd35d46e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 11 Mar 2026 20:09:03 +0100 Subject: [PATCH 018/173] some progress, now SPI outputs something at least --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 73 ++++++++++++++---------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 1 + 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index e6c18acb1e..3f84ae8871 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1769,6 +1769,19 @@ void LcdBus::setColorOrder(ColorOrder order) { #ifdef WLEDPB_SPI_SUPPORT +// low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) +static inline void spi_ll_apply_config(spi_dev_t *hw) +{ + hw->cmd.update = 1; + while (hw->cmd.update); //waiting config applied +} + +static inline void spi_ll_user_start(spi_dev_t *hw) +{ + hw->cmd.usr = 1; +} + + // Pin assignments for SPI2 quad mode on C3 // SPI2 signals: FSPID (MOSI/D0), FSPIQ (MISO/D1), FSPIWP (D2), FSPIHD (D3) static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_OUT_IDX, FSPIHD_OUT_IDX }; @@ -1776,7 +1789,7 @@ static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_O // Encoding patterns for SPI quad mode (4-step cadence, LSB first) // Each lane is one bit position in a nibble, one byte = two clock cycles, 2 bytes = one 4-step bit static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high -static constexpr uint16_t SPI_ONE_BIT = 0x0011; // output: [1,1,0,0] = 50% high +static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high SpiBusContext* SpiBusContext::_instance = nullptr; uint8_t SpiBusContext::_refCount = 0; @@ -1801,6 +1814,7 @@ void SpiBusContext::release() { SpiBusContext::SpiBusContext() : _sending(false) , _initialized(false) + , _hasStarted(false) , _currentBuffer(0) , _isrHandle(nullptr) , _hw(&GPSPI2) @@ -1819,6 +1833,7 @@ SpiBusContext::~SpiBusContext() { } bool SpiBusContext::isSpiDone() const { + if (!_hasStarted) return true; // no transfer ever started return _hw ? spi_ll_usr_is_done(_hw) : true; } @@ -1833,7 +1848,6 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; if (srcThisChunk == 0) { - if (_sending) Serial.printf("[SPI] encodeSpiChunk: done sending frame\n"); _sending = false; _framePos = 0; return; @@ -1881,13 +1895,6 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; } -// low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) -static inline void spi_ll_apply_config(spi_dev_t *hw) -{ - hw->cmd.update = 1; - while (hw->cmd.update); //waiting config applied -} - bool SpiBusContext::init(const LedTiming& timing) { if (_initialized) return true; @@ -1952,22 +1959,29 @@ bool SpiBusContext::init(const LedTiming& timing) { // We don't set it as OUTPUT manually to allow the matrix to take full control. pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + // Matching working reference order: set_mosi_bitlen BEFORE enable_mosi + // Use a default value; will be overwritten in startTransmit with actual frame size + spi_ll_set_mosi_bitlen(_hw, 16384); 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); - - // Force clear USR bit and apply config spi_ll_apply_config(_hw); Serial.printf("[SPI] init: hw->cmd.val after reset=0x%08X\n", _hw->cmd.val); - // Configure GDMA + // Configure GDMA (matching working reference: reset, connect, set desc addr, start) gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + + // Enable EOF interrupt, then install ISR (matching working reference order) + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - // Install ISR first, then enable interrupt (prevents spurious fire before handler is ready) esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, ESP_INTR_FLAG_IRAM, gdmaISR, this, &_isrHandle); @@ -1977,10 +1991,6 @@ bool SpiBusContext::init(const LedTiming& timing) { return false; } - // Enable EOF interrupt (after ISR is installed) - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - _initialized = true; Serial.printf("[SPI] Init complete\n"); return true; @@ -2059,29 +2069,24 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ } void SpiBusContext::resetAndStart() { - // Reset SPI FIFO and apply config + // Matching working reference loop restart order exactly: + // 1. Wait for any previous SPI to finish + // (caller should have ensured this, but belt-and-suspenders) + // 2. Reset SPI FIFO spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); spi_ll_apply_config(_hw); - // Restore DMA descriptor ownership (DMA clears owner after processing) + // 3. Restore DMA descriptor ownership (DMA clears owner after processing) _dmaDesc[0].owner = 1; _dmaDesc[1].owner = 1; - // Reset and restart GDMA + // 4. Reset and restart GDMA (matching working reference order) gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); - - // Re-enable EOF interrupt (channel reset may clear interrupt config) - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - - // Start SPI transfer - _hw->cmd.usr = 1; } bool SpiBusContext::startTransmit() { @@ -2104,13 +2109,14 @@ bool SpiBusContext::startTransmit() { spi_ll_set_mosi_bitlen(_hw, totalBits); + // Reset SPI + DMA first (matching working reference loop order) + resetAndStart(); + + // Then set state and encode buffers (DMA is running but SPI hasn't started yet) _sending = true; _currentBuffer = 0; encodeSpiChunk(0); encodeSpiChunk(1); - - Serial.printf("[SPI] startTransmit: frame size %u, total bits %u, resetting and starting\n", _numBytes, totalBits); - resetAndStart(); static uint32_t s_spiTxCount = 0; if (s_spiTxCount < 3 || (s_spiTxCount % 1000) == 0) { @@ -2119,6 +2125,11 @@ bool SpiBusContext::startTransmit() { } s_spiTxCount++; + _hasStarted = true; + + // Start SPI transfer LAST (matching working reference: spi_ll_user_start is the final step) + spi_ll_user_start(_hw); + return true; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 1ae1127b0a..d29cde535d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -690,6 +690,7 @@ class SpiBusContext { volatile bool _sending; bool _initialized; + bool _hasStarted; volatile uint8_t _currentBuffer; // DMA From 03d76daef3dd6ab0cdf0e90c422071a1b577acc4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 11 Mar 2026 21:41:16 +0100 Subject: [PATCH 019/173] fix SPI transfer, now basically working, still a few issues remain --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 3f84ae8871..3c48912387 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -2087,6 +2087,10 @@ void SpiBusContext::resetAndStart() { gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + + // Re-enable EOF interrupt after channel reset (reset may clear enable bits) + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; } bool SpiBusContext::startTransmit() { @@ -2109,6 +2113,11 @@ bool SpiBusContext::startTransmit() { spi_ll_set_mosi_bitlen(_hw, totalBits); + // Clear the trans_done flag BEFORE starting new transfer. + // spi_ll_usr_is_done() checks dma_int_raw.trans_done which is sticky — + // it stays set after the previous transfer until explicitly cleared. + spi_ll_clear_int_stat(_hw); + // Reset SPI + DMA first (matching working reference loop order) resetAndStart(); From 8e309a86c54d5180367742a21d38e41e9994837a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 12 Mar 2026 08:01:11 +0100 Subject: [PATCH 020/173] fix SPI output (blank out command byte), wait for spi buses to be ready to avoid multiple sendouts --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 47 ++++++++++++++++++------ wled00/src/WLEDpixelBus/WLEDpixelBus.h | 13 +++++-- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 3c48912387..28aecbd70b 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1821,6 +1821,8 @@ SpiBusContext::SpiBusContext() , _channelCount(0) , _framePos(0) , _numBytes(0) + , _stagedMask(0) + , _channelMask(0) { _dmaBuffer[0] = _dmaBuffer[1] = nullptr; for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { @@ -1937,6 +1939,21 @@ bool SpiBusContext::init(const LedTiming& timing) { spi_ll_master_set_mode(_hw, 0); spi_ll_set_tx_lsbfirst(_hw, true); + _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; + + // To prevent D2 and D3 from idling high, explicitly clear idle output polarities + _hw->ctrl.q_pol = 0; + _hw->ctrl.d_pol = 0; + _hw->ctrl.hold_pol = 0; + _hw->ctrl.wp_pol = 0; + + // To prevent WP (D2) and HD (D3) from idling high, clear quad mode in ctrl and user registers? + // Let's first clear the command/address phases to fix the initial corrupted 12 zeroes. + spi_line_mode_t linemode = {}; linemode.data_lines = 4; // quad mode spi_ll_master_set_line_mode(_hw, linemode); @@ -2040,6 +2057,7 @@ int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus) { _channels[idx].pin = pin; _channels[idx].active = true; _channelCount++; + _channelMask |= (1 << idx); // Route SPI data signal to GPIO pinMode(pin, OUTPUT); @@ -2059,12 +2077,17 @@ void SpiBusContext::unregisterChannel(int8_t channelIdx) { _channels[channelIdx] = {nullptr, -1, nullptr, 0, false}; _channelCount--; + _channelMask &= ~(1 << channelIdx); } void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; _channels[channelIdx].srcData = data; _channels[channelIdx].srcLen = len; + + // Mark this channel as staged + _stagedMask |= (1 << channelIdx); + if (len > _numBytes) _numBytes = len; } @@ -2097,6 +2120,10 @@ bool SpiBusContext::startTransmit() { if (_sending) return false; if (_channelCount == 0) return false; + // Only start transmission if ALL active channels have staged data + if (_stagedMask != _channelMask) return false; + _stagedMask = 0; // Reset for next frame + _framePos = 0; _numBytes = 0; for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { @@ -2118,21 +2145,14 @@ bool SpiBusContext::startTransmit() { // it stays set after the previous transfer until explicitly cleared. spi_ll_clear_int_stat(_hw); - // Reset SPI + DMA first (matching working reference loop order) - resetAndStart(); - - // Then set state and encode buffers (DMA is running but SPI hasn't started yet) + // Set state and encode buffers FIRST before starting DMA _sending = true; _currentBuffer = 0; encodeSpiChunk(0); encodeSpiChunk(1); - static uint32_t s_spiTxCount = 0; - if (s_spiTxCount < 3 || (s_spiTxCount % 1000) == 0) { - Serial.printf("[SPI] startTransmit #%u: %u bytes, %u totalBits\n", - s_spiTxCount, _numBytes, totalBits); - } - s_spiTxCount++; + // Reset SPI + DMA first (matching working reference loop order) + resetAndStart(); _hasStarted = true; @@ -2262,12 +2282,13 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP } _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); + return _ctx->startTransmit(); } bool ParallelSpiBus::canShow() const { if (!_ctx) return true; - return _ctx->isIdle(); + return _ctx->isIdle() && _ctx->isSpiDone(); } void ParallelSpiBus::waitComplete() { @@ -2280,6 +2301,10 @@ void ParallelSpiBus::setColorOrder(ColorOrder order) { _order = order; } +void ParallelSpiBus::setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp) { + IBus::setPixelColor(pix, c, cp); +} + #endif // WLEDPB_SPI_SUPPORT //============================================================================== diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index d29cde535d..e2927b8c71 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -88,15 +88,15 @@ enum class ColorOrder : uint8_t { RGB = 1, BRG = 2, RBG = 3, - GBR = 4, - BGR = 5, + BGR = 4, + GBR = 5, // RGBW variants (4 bytes) GRBW = 10, RGBW = 11, BRGW = 12, RBGW = 13, - GBRW = 14, - BGRW = 15, + BGRW = 14, + GBRW = 15, WRGB = 16, WGRB = 17, WBRG = 18, @@ -714,6 +714,9 @@ class SpiBusContext { size_t _framePos; // current source byte position size_t _numBytes; // total source bytes to send + uint8_t _stagedMask; + uint8_t _channelMask; + static SpiBusContext* _instance; static uint8_t _refCount; }; @@ -735,6 +738,8 @@ class ParallelSpiBus : public IBus { void waitComplete() override; const char* getType() const override { return "SPI"; } + void setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) override; + void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(ColorOrder order); From 099257020053eed03b3a2dffb0423c633e818918 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 12 Mar 2026 08:08:59 +0100 Subject: [PATCH 021/173] fix output for shorter buses (now blanks output instead of sending zeroes) --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 28aecbd70b..666af887f0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1858,15 +1858,19 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { if (!_channels[lane].active || !_channels[lane].srcData) continue; + size_t srcLen = _channels[lane].srcLen; + if (_framePos >= srcLen) continue; // Past the end of this lane's data, leave buffer 0 (low/no pulse) + + size_t validBytes = srcLen - _framePos; + if (validBytes > srcThisChunk) validBytes = srcThisChunk; + const uint16_t zerobit = SPI_ZERO_BIT << lane; const uint16_t onebit = SPI_ONE_BIT << lane; const uint8_t* src = _channels[lane].srcData; - size_t srcLen = _channels[lane].srcLen; uint16_t* pOut = reinterpret_cast(dst); - for (size_t i = 0; i < srcThisChunk; i++) { - size_t srcIdx = _framePos + i; - uint8_t v = (srcIdx < srcLen) ? src[srcIdx] : 0; + for (size_t i = 0; i < validBytes; i++) { + uint8_t v = src[_framePos + i]; *pOut++ |= (v & 0x80) ? onebit : zerobit; *pOut++ |= (v & 0x40) ? onebit : zerobit; *pOut++ |= (v & 0x20) ? onebit : zerobit; From 4486765299dc46b4021bc5cfd1a8939dda675320 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 12 Mar 2026 18:35:18 +0100 Subject: [PATCH 022/173] adding workaround "hack" for bus deadlock --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 53 +++++++++++++++++++----- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 4 +- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 666af887f0..abc50c8959 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1821,6 +1821,7 @@ SpiBusContext::SpiBusContext() , _channelCount(0) , _framePos(0) , _numBytes(0) + , _lastTransmitMs(0) , _stagedMask(0) , _channelMask(0) { @@ -1834,9 +1835,33 @@ SpiBusContext::~SpiBusContext() { deinit(); } -bool SpiBusContext::isSpiDone() const { +bool SpiBusContext::isSpiDone() { if (!_hasStarted) return true; // no transfer ever started - return _hw ? spi_ll_usr_is_done(_hw) : true; + if (_hw && !spi_ll_usr_is_done(_hw)) { + // TODO: This timeout is a workaround for underlying GDMA/SPI deadlock issues that cause stalls. + // Monitor if bumping the interrupt priority fixed the underlying starvation. Consider removing later. -> increasing ISR priority does not fix the stalls... + if (millis() - _lastTransmitMs > 100) { + forceIdle(); + return true; + } + return false; + } + return true; +} + +void SpiBusContext::forceIdle() { + _sending = false; + _stagedMask = 0; + + // Stop DMA immediately + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + if (_hw) { + spi_ll_clear_int_stat(_hw); + _hw->cmd.usr = 0; // stop user transaction + } } void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { @@ -1889,14 +1914,12 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { gdma_dev_t* dma = &GDMA; if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { - // Serial.printf("I"); // Too fast for Serial, maybe just a counter Check later - if (ctx->_currentBuffer == 0) { - ctx->encodeSpiChunk(0); - ctx->_currentBuffer = 1; - } else { - ctx->encodeSpiChunk(1); - ctx->_currentBuffer = 0; - } + uint8_t completedBuf = ctx->_currentBuffer; + ctx->encodeSpiChunk(completedBuf); + // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI + // Even if we have finished source data, DMA needs to feed 0s to SPI for the remaining frame clock length! + ctx->_dmaDesc[completedBuf].owner = 1; + ctx->_currentBuffer = completedBuf ? 0 : 1; } dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; } @@ -2004,7 +2027,7 @@ bool SpiBusContext::init(const LedTiming& timing) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, - ESP_INTR_FLAG_IRAM, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, gdmaISR, this, &_isrHandle); if (err != ESP_OK) { Serial.printf("[SPI] ISR alloc failed: %d\n", err); @@ -2128,6 +2151,14 @@ bool SpiBusContext::startTransmit() { if (_stagedMask != _channelMask) return false; _stagedMask = 0; // Reset for next frame + // Pre-emptively stop DMA and disable its interrupt so a late ISR from a previous + // frame's lingering zeroes doesn't trigger while we set up the new frame + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + _lastTransmitMs = millis(); _framePos = 0; _numBytes = 0; for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index e2927b8c71..caf7b1b126 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -675,7 +675,8 @@ class SpiBusContext { bool startTransmit(); bool isIdle() const { return !_sending; } - bool isSpiDone() const; + bool isSpiDone(); + void forceIdle(); void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); @@ -713,6 +714,7 @@ class SpiBusContext { uint8_t _channelCount; size_t _framePos; // current source byte position size_t _numBytes; // total source bytes to send + mutable uint32_t _lastTransmitMs; uint8_t _stagedMask; uint8_t _channelMask; From 4f56f1efe5e66760d334373b8972e7a007caf6fe Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 12 Mar 2026 19:44:37 +0100 Subject: [PATCH 023/173] add missing pinmanager allocation for digital pin --- wled00/bus_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 8c3cb7310b..9a7ed79c04 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -136,7 +136,7 @@ BusDigital::BusDigital(const BusConfig &bc) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial 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]; From 8a93cffb0900ae1af1a8a03ecf58ace34d62d8cf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 12 Mar 2026 21:19:03 +0100 Subject: [PATCH 024/173] SPI DMA now mostly working --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 205 +++++++++++++++++------ wled00/src/WLEDpixelBus/WLEDpixelBus.h | 4 + 2 files changed, 162 insertions(+), 47 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index abc50c8959..6f1ce8400a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1816,7 +1816,9 @@ SpiBusContext::SpiBusContext() , _initialized(false) , _hasStarted(false) , _currentBuffer(0) + , _descCount(0) , _isrHandle(nullptr) + , _spiIsrHandle(nullptr) , _hw(&GPSPI2) , _channelCount(0) , _framePos(0) @@ -1837,38 +1839,56 @@ SpiBusContext::~SpiBusContext() { bool SpiBusContext::isSpiDone() { if (!_hasStarted) return true; // no transfer ever started - if (_hw && !spi_ll_usr_is_done(_hw)) { - // TODO: This timeout is a workaround for underlying GDMA/SPI deadlock issues that cause stalls. - // Monitor if bumping the interrupt priority fixed the underlying starvation. Consider removing later. -> increasing ISR priority does not fix the stalls... - if (millis() - _lastTransmitMs > 100) { - forceIdle(); - return true; - } - return false; + + // We are logically done once all real data is encoded + if (!_sending) { + return true; } - return true; + + // Fail-safe watchdog: if stuck for > 250ms, recover it + if (millis() - _lastTransmitMs > 250) { + forceIdle(); + return true; + } + + return false; } void SpiBusContext::forceIdle() { _sending = false; _stagedMask = 0; - + // Stop DMA immediately gdma_dev_t* dma = &GDMA; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - + if (_hw) { - spi_ll_clear_int_stat(_hw); - _hw->cmd.usr = 0; // stop user transaction + _hw->cmd.usr = 0; + _hw->dma_int_clr.val = 0xFFFFFFFF; } } +void SpiBusContext::dumpDebug() { + Serial.printf("\n--- SPI GDMA DEBUG ---\n"); + Serial.printf("_sending=%d, _framePos=%u, _numBytes=%u, _hasStarted=%d\n", _sending, _framePos, _numBytes, _hasStarted); + Serial.printf("GDMA INTR ST=0x%08X, ENA=0x%08X\n", GDMA.intr[WLEDPB_SPI_GDMA_CHANNEL].st.val, GDMA.intr[WLEDPB_SPI_GDMA_CHANNEL].ena.val); + if (_hw) { + Serial.printf("SPI cmd.usr=%d, dma_int_raw=0x%08X\n", _hw->cmd.usr, _hw->dma_int_raw.val); + } + Serial.printf("Descriptors:\n"); + Serial.printf(" Desc0: owner=%d length=%d eof=%d addr=0x%p next=0x%p\n", _dmaDesc[0].owner, _dmaDesc[0].length, _dmaDesc[0].eof, &_dmaDesc[0], _dmaDesc[0].qe.stqe_next); + Serial.printf(" Desc1: owner=%d length=%d eof=%d addr=0x%p next=0x%p\n", _dmaDesc[1].owner, _dmaDesc[1].length, _dmaDesc[1].eof, &_dmaDesc[1], _dmaDesc[1].qe.stqe_next); + Serial.printf("----------------------\n"); +} + void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { uint8_t* dst = _dmaBuffer[bufIdx]; memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); - if (!_sending) return; + if (!_sending) { + return; + } size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; @@ -1909,6 +1929,55 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { _framePos += srcThisChunk; } +// SPI interrupt ISR: handles both trans_done (bit counter expired) and +// outfifo_empty_err (FIFO underrun from ISR latency, e.g. WiFi traffic). +// On trans_done: just reload bit length and restart. Re-enable outfifo_empty_err if it was disabled. +// On outfifo_empty_err: reset FIFO, reset DMA, restart. DISABLE the outfifo_empty_err interrupt +// to prevent an infinite ISR loop (the error re-fires immediately if WiFi is still hogging CPU). +// trans_done will re-enable it on the next successful cycle. +void IRAM_ATTR SpiBusContext::spiISR(void* arg) { + SpiBusContext* ctx = (SpiBusContext*)arg; + uint32_t raw = ctx->_hw->dma_int_raw.val; + + if (raw & 0x02) { + // outfifo_empty_err (bit 1): SPI FIFO starved because DMA couldn't keep up. + // Temporarily disable this interrupt to prevent infinite ISR loop / WDT crash. + ctx->_hw->dma_int_ena.outfifo_empty_err = 0; + ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags + ctx->_hw->cmd.usr = 0; // Stop SPI + + spi_ll_dma_tx_fifo_reset(ctx->_hw); + spi_ll_outfifo_empty_clr(ctx->_hw); + + // Reset and restart DMA from current descriptor + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + ctx->_dmaDesc[0].owner = 1; + ctx->_dmaDesc[1].owner = 1; + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&ctx->_dmaDesc[ctx->_currentBuffer]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + // Restart SPI with max buffer-aligned bit count (31 * 1024 * 8 = 253952) + // to minimize trans_done restart gaps + spi_ll_clear_int_stat(ctx->_hw); + spi_ll_set_mosi_bitlen(ctx->_hw, 253952); + ctx->_hw->cmd.usr = 1; + return; + } + + if (raw & 0x1000) { + // trans_done (bit 12): SPI finished counting its bits. Restart immediately. + ctx->_hw->dma_int_clr.trans_done = 1; + spi_ll_set_mosi_bitlen(ctx->_hw, 253952); + ctx->_hw->cmd.usr = 1; + // Re-enable outfifo_empty_err now that we've had a successful cycle + ctx->_hw->dma_int_ena.outfifo_empty_err = 1; + } +} + void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; gdma_dev_t* dma = &GDMA; @@ -1916,8 +1985,11 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { uint8_t completedBuf = ctx->_currentBuffer; ctx->encodeSpiChunk(completedBuf); + // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI - // Even if we have finished source data, DMA needs to feed 0s to SPI for the remaining frame clock length! + ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + ctx->_dmaDesc[completedBuf].eof = 1; ctx->_dmaDesc[completedBuf].owner = 1; ctx->_currentBuffer = completedBuf ? 0 : 1; } @@ -2030,7 +2102,22 @@ bool SpiBusContext::init(const LedTiming& timing) { ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, gdmaISR, this, &_isrHandle); if (err != ESP_OK) { - Serial.printf("[SPI] ISR alloc failed: %d\n", err); + Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + deinit(); + return false; + } + + // Install SPI ISR for trans_done and outfifo_empty_err recovery. + // trans_done: SPI finished its bit counter, restart immediately. + // outfifo_empty_err: FIFO underrun (e.g. WiFi ISR delayed DMA), must reset and recover. + _hw->dma_int_clr.val = 0xFFFFFFFF; // Clear all flags + _hw->dma_int_ena.trans_done = 1; + _hw->dma_int_ena.outfifo_empty_err = 1; + err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, + spiISR, this, &_spiIsrHandle); + if (err != ESP_OK) { + Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); deinit(); return false; } @@ -2053,6 +2140,15 @@ void SpiBusContext::deinit() { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + if (_hw) { + _hw->dma_int_ena.trans_done = 0; // Disable SPI trans_done interrupt + } + + if (_spiIsrHandle) { + esp_intr_free(_spiIsrHandle); + _spiIsrHandle = nullptr; + } + if (_isrHandle) { esp_intr_free(_isrHandle); _isrHandle = nullptr; @@ -2151,48 +2247,58 @@ bool SpiBusContext::startTransmit() { if (_stagedMask != _channelMask) return false; _stagedMask = 0; // Reset for next frame - // Pre-emptively stop DMA and disable its interrupt so a late ISR from a previous - // frame's lingering zeroes doesn't trigger while we set up the new frame - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - _lastTransmitMs = millis(); - _framePos = 0; - _numBytes = 0; + size_t newBytes = 0; for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcLen > _numBytes) { - _numBytes = _channels[ch].srcLen; + if (_channels[ch].active && _channels[ch].srcLen > newBytes) { + newBytes = _channels[ch].srcLen; } } - // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte - // Plus reset: extra zero bits at the end - uint32_t resetBits = 5000; // ~300us reset at ~2.6MHz - uint32_t totalBits = (_numBytes * 16 * 8) + resetBits; - if (totalBits > 262143) totalBits = 262143; // SPI max 18 bits for length register + if (!_hasStarted) { + _framePos = 0; + _numBytes = newBytes; + + // Set totalBits to max buffer-aligned value (31 * 1024 * 8 = 253952 bits). + // This gives ~97ms of continuous output before trans_done fires and restarts. + // If FIFO underruns (WiFi traffic etc), outfifo_empty_err fires and recovers. + uint32_t totalBits = 253952; + + spi_ll_set_mosi_bitlen(_hw, totalBits); + + // Clear the trans_done flag BEFORE starting new transfer. + // spi_ll_usr_is_done() checks dma_int_raw.trans_done which is sticky — + // it stays set after the previous transfer until explicitly cleared. + spi_ll_clear_int_stat(_hw); - spi_ll_set_mosi_bitlen(_hw, totalBits); + // Set state and encode buffers FIRST before starting DMA + _sending = true; + _currentBuffer = 0; + encodeSpiChunk(0); + encodeSpiChunk(1); - // Clear the trans_done flag BEFORE starting new transfer. - // spi_ll_usr_is_done() checks dma_int_raw.trans_done which is sticky — - // it stays set after the previous transfer until explicitly cleared. - spi_ll_clear_int_stat(_hw); + // Reset SPI + DMA first (matching working reference loop order) + resetAndStart(); - // Set state and encode buffers FIRST before starting DMA - _sending = true; - _currentBuffer = 0; - encodeSpiChunk(0); - encodeSpiChunk(1); + _hasStarted = true; - // Reset SPI + DMA first (matching working reference loop order) - resetAndStart(); + // Start SPI transfer LAST (matching working reference: spi_ll_user_start is the final step) + spi_ll_user_start(_hw); + } else { + // Infinite send active! + // The hardware is already running, churning out zeros via the background ISR loop. + // All we need to do is reset the pointers and flip _sending = true. The ISR + // will naturally pick this up and seamlessly transition from sending zeros to legitimate frame data! + + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Temporarily pause ISR to prevent race condition - _hasStarted = true; + _framePos = 0; + _numBytes = newBytes; + _sending = true; // Engage new frame - // Start SPI transfer LAST (matching working reference: spi_ll_user_start is the final step) - spi_ll_user_start(_hw); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; // Unpause ISR + } return true; } @@ -2290,6 +2396,9 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP while (!_ctx->isIdle()) { if (millis() - startWait > 500) { Serial.printf("[SPI] show() timeout waiting for idle\n"); + _ctx->dumpDebug(); + // Force idle to prevent endless freeze + _ctx->forceIdle(); return false; } vTaskDelay(1); @@ -2300,6 +2409,8 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP while (!_ctx->isSpiDone()) { if (millis() - startWait > 500) { Serial.printf("[SPI] show() timeout waiting for SpiDone\n"); + _ctx->dumpDebug(); + _ctx->forceIdle(); return false; } vTaskDelay(1); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index caf7b1b126..5ad91d7dca 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -677,6 +677,7 @@ class SpiBusContext { bool isIdle() const { return !_sending; } bool isSpiDone(); void forceIdle(); + void dumpDebug(); void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); @@ -688,16 +689,19 @@ class SpiBusContext { void resetAndStart(); static void IRAM_ATTR gdmaISR(void* arg); + static void IRAM_ATTR spiISR(void* arg); volatile bool _sending; bool _initialized; bool _hasStarted; volatile uint8_t _currentBuffer; + volatile uint8_t _descCount; // DMA uint8_t* _dmaBuffer[2]; lldesc_t _dmaDesc[2]; intr_handle_t _isrHandle; + intr_handle_t _spiIsrHandle; // SPI device spi_dev_t* _hw; From 6a8c769e4af9158542103833a323c02e78ce1117 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 08:12:20 +0100 Subject: [PATCH 025/173] cleanup --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 108 ++++------------------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 2 - 2 files changed, 16 insertions(+), 94 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 6f1ce8400a..db99baf05d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1816,7 +1816,6 @@ SpiBusContext::SpiBusContext() , _initialized(false) , _hasStarted(false) , _currentBuffer(0) - , _descCount(0) , _isrHandle(nullptr) , _spiIsrHandle(nullptr) , _hw(&GPSPI2) @@ -1869,19 +1868,6 @@ void SpiBusContext::forceIdle() { } } -void SpiBusContext::dumpDebug() { - Serial.printf("\n--- SPI GDMA DEBUG ---\n"); - Serial.printf("_sending=%d, _framePos=%u, _numBytes=%u, _hasStarted=%d\n", _sending, _framePos, _numBytes, _hasStarted); - Serial.printf("GDMA INTR ST=0x%08X, ENA=0x%08X\n", GDMA.intr[WLEDPB_SPI_GDMA_CHANNEL].st.val, GDMA.intr[WLEDPB_SPI_GDMA_CHANNEL].ena.val); - if (_hw) { - Serial.printf("SPI cmd.usr=%d, dma_int_raw=0x%08X\n", _hw->cmd.usr, _hw->dma_int_raw.val); - } - Serial.printf("Descriptors:\n"); - Serial.printf(" Desc0: owner=%d length=%d eof=%d addr=0x%p next=0x%p\n", _dmaDesc[0].owner, _dmaDesc[0].length, _dmaDesc[0].eof, &_dmaDesc[0], _dmaDesc[0].qe.stqe_next); - Serial.printf(" Desc1: owner=%d length=%d eof=%d addr=0x%p next=0x%p\n", _dmaDesc[1].owner, _dmaDesc[1].length, _dmaDesc[1].eof, &_dmaDesc[1], _dmaDesc[1].qe.stqe_next); - Serial.printf("----------------------\n"); -} - void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { uint8_t* dst = _dmaBuffer[bufIdx]; memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); @@ -1929,19 +1915,15 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { _framePos += srcThisChunk; } -// SPI interrupt ISR: handles both trans_done (bit counter expired) and -// outfifo_empty_err (FIFO underrun from ISR latency, e.g. WiFi traffic). -// On trans_done: just reload bit length and restart. Re-enable outfifo_empty_err if it was disabled. -// On outfifo_empty_err: reset FIFO, reset DMA, restart. DISABLE the outfifo_empty_err interrupt -// to prevent an infinite ISR loop (the error re-fires immediately if WiFi is still hogging CPU). -// trans_done will re-enable it on the next successful cycle. +// SPI ISR: handles trans_done (restart SPI bit counter) and outfifo_empty_err +// (FIFO underrun recovery). outfifo_empty_err is temporarily disabled after +// handling to prevent infinite ISR loops; trans_done re-enables it. void IRAM_ATTR SpiBusContext::spiISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; uint32_t raw = ctx->_hw->dma_int_raw.val; if (raw & 0x02) { - // outfifo_empty_err (bit 1): SPI FIFO starved because DMA couldn't keep up. - // Temporarily disable this interrupt to prevent infinite ISR loop / WDT crash. + // outfifo_empty_err: FIFO starved, disable to prevent ISR loop ctx->_hw->dma_int_ena.outfifo_empty_err = 0; ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags ctx->_hw->cmd.usr = 0; // Stop SPI @@ -1949,7 +1931,6 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { spi_ll_dma_tx_fifo_reset(ctx->_hw); spi_ll_outfifo_empty_clr(ctx->_hw); - // Reset and restart DMA from current descriptor gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); ctx->_dmaDesc[0].owner = 1; @@ -1960,8 +1941,6 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - // Restart SPI with max buffer-aligned bit count (31 * 1024 * 8 = 253952) - // to minimize trans_done restart gaps spi_ll_clear_int_stat(ctx->_hw); spi_ll_set_mosi_bitlen(ctx->_hw, 253952); ctx->_hw->cmd.usr = 1; @@ -1969,11 +1948,9 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { } if (raw & 0x1000) { - // trans_done (bit 12): SPI finished counting its bits. Restart immediately. ctx->_hw->dma_int_clr.trans_done = 1; spi_ll_set_mosi_bitlen(ctx->_hw, 253952); ctx->_hw->cmd.usr = 1; - // Re-enable outfifo_empty_err now that we've had a successful cycle ctx->_hw->dma_int_ena.outfifo_empty_err = 1; } } @@ -1999,8 +1976,6 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { bool SpiBusContext::init(const LedTiming& timing) { if (_initialized) return true; - Serial.printf("[SPI] Initializing SPI parallel bus context\n"); - // Allocate DMA buffers for (int i = 0; i < 2; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, @@ -2044,15 +2019,12 @@ bool SpiBusContext::init(const LedTiming& timing) { _hw->user.usr_miso = 0; _hw->user.usr_mosi = 1; - // To prevent D2 and D3 from idling high, explicitly clear idle output polarities + // 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; - // To prevent WP (D2) and HD (D3) from idling high, clear quad mode in ctrl and user registers? - // Let's first clear the command/address phases to fix the initial corrupted 12 zeroes. - spi_line_mode_t linemode = {}; linemode.data_lines = 4; // quad mode spi_ll_master_set_line_mode(_hw, linemode); @@ -2068,15 +2040,9 @@ bool SpiBusContext::init(const LedTiming& timing) { spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); - Serial.printf("[SPI] Bit period: %uns, target SPI freq: %uHz\n", bitPeriodNs, targetFreq); - // Route SPI clock to a dummy pin (needed for DMA to work) - // GPIO11 is VDD_SPI on C3 but routing CLK to it doesn't affect power. - // We don't set it as OUTPUT manually to allow the matrix to take full control. pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); - // Matching working reference order: set_mosi_bitlen BEFORE enable_mosi - // Use a default value; will be overwritten in startTransmit with actual frame size spi_ll_set_mosi_bitlen(_hw, 16384); spi_ll_enable_mosi(_hw, true); @@ -2084,17 +2050,14 @@ bool SpiBusContext::init(const LedTiming& timing) { spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); spi_ll_apply_config(_hw); - - Serial.printf("[SPI] init: hw->cmd.val after reset=0x%08X\n", _hw->cmd.val); - // Configure GDMA (matching working reference: reset, connect, set desc addr, start) + // Configure GDMA gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - // Enable EOF interrupt, then install ISR (matching working reference order) dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; @@ -2107,10 +2070,8 @@ bool SpiBusContext::init(const LedTiming& timing) { return false; } - // Install SPI ISR for trans_done and outfifo_empty_err recovery. - // trans_done: SPI finished its bit counter, restart immediately. - // outfifo_empty_err: FIFO underrun (e.g. WiFi ISR delayed DMA), must reset and recover. - _hw->dma_int_clr.val = 0xFFFFFFFF; // Clear all flags + // 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; err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, @@ -2123,12 +2084,10 @@ bool SpiBusContext::init(const LedTiming& timing) { } _initialized = true; - Serial.printf("[SPI] Init complete\n"); return true; } void SpiBusContext::deinit() { - Serial.printf("[SPI] deinit() starting\n"); _sending = false; // Stop SPI and DMA before freeing resources @@ -2186,7 +2145,6 @@ int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus) { pinMode(pin, OUTPUT); pinMatrixOutAttach(pin, SPI_SIGNAL_INDICES[idx], false, false); - Serial.printf("[SPI] Registered channel %d on pin %d (signal=%d)\n", idx, pin, SPI_SIGNAL_INDICES[idx]); return idx; } @@ -2215,26 +2173,19 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ } void SpiBusContext::resetAndStart() { - // Matching working reference loop restart order exactly: - // 1. Wait for any previous SPI to finish - // (caller should have ensured this, but belt-and-suspenders) - // 2. Reset SPI FIFO spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); spi_ll_apply_config(_hw); - // 3. Restore DMA descriptor ownership (DMA clears owner after processing) _dmaDesc[0].owner = 1; _dmaDesc[1].owner = 1; - // 4. Reset and restart GDMA (matching working reference order) gdma_dev_t* dma = &GDMA; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - // Re-enable EOF interrupt after channel reset (reset may clear enable bits) dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; } @@ -2243,9 +2194,8 @@ bool SpiBusContext::startTransmit() { if (_sending) return false; if (_channelCount == 0) return false; - // Only start transmission if ALL active channels have staged data if (_stagedMask != _channelMask) return false; - _stagedMask = 0; // Reset for next frame + _stagedMask = 0; _lastTransmitMs = millis(); size_t newBytes = 0; @@ -2259,45 +2209,27 @@ bool SpiBusContext::startTransmit() { _framePos = 0; _numBytes = newBytes; - // Set totalBits to max buffer-aligned value (31 * 1024 * 8 = 253952 bits). - // This gives ~97ms of continuous output before trans_done fires and restarts. - // If FIFO underruns (WiFi traffic etc), outfifo_empty_err fires and recovers. - uint32_t totalBits = 253952; - - spi_ll_set_mosi_bitlen(_hw, totalBits); - - // Clear the trans_done flag BEFORE starting new transfer. - // spi_ll_usr_is_done() checks dma_int_raw.trans_done which is sticky — - // it stays set after the previous transfer until explicitly cleared. + // Max buffer-aligned bit count: continuous output for ~97ms before + // trans_done restarts. outfifo_empty_err handles FIFO underruns. + spi_ll_set_mosi_bitlen(_hw, 253952); spi_ll_clear_int_stat(_hw); - // Set state and encode buffers FIRST before starting DMA _sending = true; _currentBuffer = 0; encodeSpiChunk(0); encodeSpiChunk(1); - // Reset SPI + DMA first (matching working reference loop order) resetAndStart(); - _hasStarted = true; - - // Start SPI transfer LAST (matching working reference: spi_ll_user_start is the final step) spi_ll_user_start(_hw); } else { - // Infinite send active! - // The hardware is already running, churning out zeros via the background ISR loop. - // All we need to do is reset the pointers and flip _sending = true. The ISR - // will naturally pick this up and seamlessly transition from sending zeros to legitimate frame data! - + // Hardware already running - just update pointers, ISR picks up new data gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Temporarily pause ISR to prevent race condition - + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; _framePos = 0; _numBytes = newBytes; - _sending = true; // Engage new frame - - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; // Unpause ISR + _sending = true; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; } return true; @@ -2342,19 +2274,16 @@ bool ParallelSpiBus::begin() { } _initialized = true; - Serial.printf("[SPI] ParallelSpiBus::begin() OK: pin=%d, channel=%d\n", _pin, _channelIdx); return true; } void ParallelSpiBus::end() { - Serial.printf("[SPI] ParallelSpiBus::end() pin=%d\n", _pin); if (!_initialized) return; if (_ctx) { uint32_t startWait = millis(); while (!_ctx->isIdle()) { if (millis() - startWait > 500) { - Serial.printf("[SPI] end() timeout waiting for idle, forcing...\n"); break; } vTaskDelay(1); @@ -2395,9 +2324,6 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP uint32_t startWait = millis(); while (!_ctx->isIdle()) { if (millis() - startWait > 500) { - Serial.printf("[SPI] show() timeout waiting for idle\n"); - _ctx->dumpDebug(); - // Force idle to prevent endless freeze _ctx->forceIdle(); return false; } @@ -2408,8 +2334,6 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP startWait = millis(); while (!_ctx->isSpiDone()) { if (millis() - startWait > 500) { - Serial.printf("[SPI] show() timeout waiting for SpiDone\n"); - _ctx->dumpDebug(); _ctx->forceIdle(); return false; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 5ad91d7dca..9dd93ce260 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -677,7 +677,6 @@ class SpiBusContext { bool isIdle() const { return !_sending; } bool isSpiDone(); void forceIdle(); - void dumpDebug(); void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); @@ -695,7 +694,6 @@ class SpiBusContext { bool _initialized; bool _hasStarted; volatile uint8_t _currentBuffer; - volatile uint8_t _descCount; // DMA uint8_t* _dmaBuffer[2]; From 2dee50a091448fc0a711cc4610458c483cd3f120 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 10:06:58 +0100 Subject: [PATCH 026/173] Fix LCD output for S3, now working --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 379 +++++++---------------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 27 +- 2 files changed, 119 insertions(+), 287 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index db99baf05d..bf9fb0f812 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1034,7 +1034,8 @@ Key design: #ifdef WLEDPB_LCD_SUPPORT -#include "esp_private/periph_ctrl.h" +#include "driver/periph_ctrl.h" // works on S3 +//#include "esp_private/periph_ctrl.h" // works on C3, todo: use ifdefs #include "esp_private/gdma.h" #include "esp_rom_gpio.h" #include "hal/dma_types.h" @@ -1047,18 +1048,6 @@ Key design: // Configuration // ============================================ -#ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE 512 -#endif - -#ifndef WLEDPB_LCD_CADENCE_STEPS -#define WLEDPB_LCD_CADENCE_STEPS 4 -#endif - -#ifndef WLEDPB_LCD_DEBUG -#define WLEDPB_LCD_DEBUG 1 -#endif - #if WLEDPB_LCD_DEBUG #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) #else @@ -1071,10 +1060,7 @@ static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); -// Debug counters -static volatile uint32_t s_isrCount = 0; -static volatile uint32_t s_dataFills = 0; -static volatile uint32_t s_zeroFills = 0; + LcdBusContext* LcdBusContext::_instance = nullptr; uint8_t LcdBusContext::_refCount = 0; @@ -1098,18 +1084,16 @@ void LcdBusContext::release() { LcdBusContext::LcdBusContext() : _state(DriverState::Idle) - , _txState(TxState::Idle) , _initialized(false) , _use16Bit(false) , _dmaChannel(nullptr) , _timing{0, 0, 0, 0, 0} + , _bufferSize(WLEDPB_LCD_DMA_BUFFER_SIZE) , _channelCount(0) , _channelMask(0) , _stagedMask(0) , _maxDataLen(0) , _activeBuffer(0) - , _resetBytesRemaining(0) - , _resetBytesPerBuffer(0) { for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; @@ -1125,37 +1109,21 @@ LcdBusContext::~LcdBusContext() { } bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16Bit) { - if (_initialized) { - LCD_LOG("Already initialized"); - return true; - } + if (_initialized) return true; - LCD_LOG("Initializing LCD bus context (continuous DMA mode)"); - LCD_LOG(" DMA buffer size: %u bytes x2", WLEDPB_LCD_DMA_BUFFER_SIZE); - LCD_LOG(" Cadence: %d-step", WLEDPB_LCD_CADENCE_STEPS); + LCD_LOG("Init: buf=%u x2, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", + WLEDPB_LCD_DMA_BUFFER_SIZE, WLEDPB_LCD_CADENCE_STEPS, + timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); _timing = timing; _use16Bit = use16Bit; + _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; - // Calculate reset bytes needed - // Reset time in us -> bytes at our clock rate - // Clock period = bitPeriod / cadenceSteps - // Bytes per us = 1000 / clockPeriodNs uint32_t bitPeriodNs = timing.bitPeriod(); - uint32_t clockPeriodNs = bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS; - uint32_t bytesPerUs = 1000 / clockPeriodNs; - _resetBytesPerBuffer = timing.reset_us * bytesPerUs; - - // Round up to buffer size for clean stopping - if (_resetBytesPerBuffer < WLEDPB_LCD_DMA_BUFFER_SIZE) { - _resetBytesPerBuffer = WLEDPB_LCD_DMA_BUFFER_SIZE; - } - - LCD_LOG(" Reset time: %u us = %u bytes minimum", timing.reset_us, _resetBytesPerBuffer); // Allocate double DMA buffers for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_malloc(WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); if (!_dmaBuffer[i]) { LCD_LOG("ERROR: DMA buffer %d alloc failed", i); deinit(); @@ -1183,7 +1151,7 @@ bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16B _dmaDesc[i]->next = _dmaDesc[i ^ 1]; // Point to other buffer (circular) } - LCD_LOG(" DMA descriptors configured in circular mode"); + // Enable LCD_CAM peripheral periph_module_enable(PERIPH_LCD_CAM_MODULE); @@ -1272,18 +1240,17 @@ bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16B gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); _initialized = true; - LCD_LOG("LCD bus initialized (continuous circular DMA)"); - + LCD_LOG("Init OK: clkm_div=%u+%u/%u", + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_num, + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_b, + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_a); return true; } void LcdBusContext::deinit() { - LCD_LOG("Deinitializing LCD"); - // Stop transmission LCD_CAM.lcd_user.lcd_start = 0; _state = DriverState::Idle; - _txState = TxState::Idle; if (_dmaChannel) { gdma_stop(_dmaChannel); @@ -1310,21 +1277,15 @@ void LcdBusContext::deinit() { _initialized = false; } -int8_t LcdBusContext:: registerChannel(int8_t pin, LcdBus* bus) { - LCD_LOG("Registering channel: pin=%d", pin); - +int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus) { int8_t idx = -1; for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - if (! _channels[i].active) { + if (!_channels[i].active) { idx = i; break; } } - - if (idx < 0) { - LCD_LOG("ERROR: No free channels"); - return -1; - } + if (idx < 0) return -1; _channels[idx].bus = bus; _channels[idx].pin = pin; @@ -1332,12 +1293,11 @@ int8_t LcdBusContext:: registerChannel(int8_t pin, LcdBus* bus) { _channelCount++; _channelMask |= (1 << idx); - uint8_t muxIdx = LCD_DATA_OUT0_IDX + idx; - esp_rom_gpio_connect_out_signal(pin, muxIdx, false, false); + esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, false, false); gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); - LCD_LOG(" Channel %d registered: pin=%d", idx, pin); + LCD_LOG("Channel %d: pin=%d, mask=0x%02X", idx, pin, _channelMask); return idx; } @@ -1355,214 +1315,103 @@ void LcdBusContext::unregisterChannel(int8_t channelIdx) { _channelMask &= ~(1 << channelIdx); } -void LcdBusContext:: setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { +void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; _channels[channelIdx].srcData = data; _channels[channelIdx].srcLen = len; _channels[channelIdx].srcPos = 0; - if (len > _maxDataLen) { - _maxDataLen = len; - } + if (len > _maxDataLen) _maxDataLen = len; - if (_stagedMask & (1 << channelIdx)) { - _stagedMask = 0; - } + if (_stagedMask & (1 << channelIdx)) _stagedMask = 0; _stagedMask |= (1 << channelIdx); } -size_t IRAM_ATTR LcdBusContext:: encodeIntoBuffer(uint8_t* dest, size_t destLen) { - // Encode pixel data from all channels - // Returns number of bytes written (0 if no more data) - - size_t written = 0; - const size_t bytesPerSourceByte = 8 * WLEDPB_LCD_CADENCE_STEPS; - - // Clear buffer (important for OR operations and zero-fill) +void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { + // 4-step cadence encoding for parallel output without byte swapping + // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) + // Desired output: [HIGH][data][data][LOW] for each bit + // Buffer is always filled completely (zeros = LOW = reset signal) + memset(dest, 0, destLen); - - while (written + bytesPerSourceByte <= destLen) { - // Check if any channel has data + size_t pos = 0; + + // Process each source byte position across all channels + while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte bool hasData = false; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - hasData = true; - break; - } - } - - if (! hasData) break; - - // Encode one byte from each active channel - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (! _channels[ch].active) continue; + if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - + + hasData = true; uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; uint8_t chMask = (1 << ch); - uint8_t* p = dest + written; - + + uint8_t* p = dest + pos; for (int bit = 7; bit >= 0; bit--) { uint8_t dataVal = (srcByte >> bit) & 1; - -#if WLEDPB_LCD_CADENCE_STEPS == 4 + + // Step 0 (HIGH) p[0] |= chMask; - if (dataVal) { - p[1] |= chMask; - p[2] |= chMask; - } + // Step 1 (data) + if (dataVal) p[1] |= chMask; + // Step 2 (data) + if (dataVal) p[2] |= chMask; + // Step 3 (LOW) - already 0 p += 4; -#else - p[0] |= chMask; - if (dataVal) { - p[1] |= chMask; - } - p += 3; -#endif } } - - // Advance all channels + + if (!hasData) break; + + // Advance all channel positions for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { _channels[ch].srcPos++; } } - - written += bytesPerSourceByte; - } - - return written; -} -bool IRAM_ATTR LcdBusContext::hasMoreData() const { - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - return true; - } + pos += 32; } - return false; + // Rest of buffer remains zero (reset signal) from memset } -void IRAM_ATTR LcdBusContext::fillBufferForState(uint8_t bufIdx) { - // Fill buffer based on current transmission state - // This is called from ISR context! - - switch (_txState) { - case TxState::SendingData: { - size_t encoded = encodeIntoBuffer(_dmaBuffer[bufIdx], WLEDPB_LCD_DMA_BUFFER_SIZE); - - if (encoded > 0) { - s_dataFills++; - // Still have data, continue - if (! hasMoreData()) { - // This was the last data, switch to reset phase - _txState = TxState::SendingReset; - _resetBytesRemaining = _resetBytesPerBuffer; - } - } else { - // No data encoded, switch to reset - _txState = TxState::SendingReset; - _resetBytesRemaining = _resetBytesPerBuffer; - s_zeroFills++; - // Buffer already zeroed by encodeIntoBuffer - } - break; - } - - case TxState:: SendingReset: { - // Fill with zeros for reset period - memset(_dmaBuffer[bufIdx], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); - s_zeroFills++; - - if (_resetBytesRemaining <= WLEDPB_LCD_DMA_BUFFER_SIZE) { - // This is the last reset buffer, stop after it completes - _txState = TxState::StopPending; - _resetBytesRemaining = 0; - } else { - _resetBytesRemaining -= WLEDPB_LCD_DMA_BUFFER_SIZE; - } - break; - } - - case TxState::StopPending: { - // Fill with zeros but we'll stop after this - memset(_dmaBuffer[bufIdx], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); - s_zeroFills++; - break; - } - - default: - break; - } +void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { + encode4Step(_dmaBuffer[bufIdx], _bufferSize); } bool LcdBusContext::startTransmit() { - if (_stagedMask != _channelMask) { - return false; // wait for all channels (from multiple buses) - } - - _stagedMask = 0; // ready for next frame once we transmit + if (_stagedMask != _channelMask) return false; // wait for all channels + _stagedMask = 0; - if (_state != DriverState::Idle) { - LCD_LOG("ERROR: Not idle"); - return false; - } - - if (_channelCount == 0) { - LCD_LOG("ERROR: No channels"); - return false; - } + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; // Reset channel positions _maxDataLen = 0; for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { if (_channels[ch].active) { _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) { - _maxDataLen = _channels[ch].srcLen; - } + if (_channels[ch].srcLen > _maxDataLen) _maxDataLen = _channels[ch].srcLen; } } + if (_maxDataLen == 0) return false; - if (_maxDataLen == 0) { - LCD_LOG("ERROR: No data"); - return false; - } - - // Reset debug counters - s_isrCount = 0; - s_dataFills = 0; - s_zeroFills = 0; + // Fill both buffers initially + fillBuffer(0); + fillBuffer(1); - LCD_LOG("Starting transmission: %u bytes", _maxDataLen); + _dmaDesc[0]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[1]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - // Set initial state - _txState = TxState::SendingData; _activeBuffer = 0; - _resetBytesRemaining = 0; - - // Pre-fill both buffers with data - // Buffer 0: First chunk - size_t len0 = encodeIntoBuffer(_dmaBuffer[0], WLEDPB_LCD_DMA_BUFFER_SIZE); - s_dataFills++; - - if (! hasMoreData()) { - // All data fits in buffer 0, prepare for reset - _txState = TxState::SendingReset; - _resetBytesRemaining = _resetBytesPerBuffer; - } - - // Buffer 1: Second chunk or start of reset - fillBufferForState(1); - _state = DriverState::Sending; // Start DMA (circular mode - descriptors already linked) - // DO NOT call gdma_reset here - just start fresh 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; @@ -1571,43 +1420,56 @@ bool LcdBusContext::startTransmit() { esp_rom_delay_us(1); LCD_CAM.lcd_user.lcd_start = 1; - LCD_LOG(" DMA started in circular mode"); - return true; } -IRAM_ATTR bool LcdBusContext:: dmaCallback(gdma_channel_handle_t dma_chan, +IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data) { LcdBusContext* ctx = (LcdBusContext*)user_data; - - s_isrCount++; - - // The buffer that just completed is the active one - // We need to refill it while the other buffer is being sent + + // The completed buffer just finished playing; DMA is now on the other buffer uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer ^= 1; // Switch to other buffer - - if (ctx->_txState == TxState::StopPending) { - // Stop transmission cleanly + ctx->_activeBuffer ^= 1; + + if (ctx->_state == DriverState::Sending) { + // Encode next chunk into the completed buffer + ctx->fillBuffer(completedBuf); + + // Check if all source data has been consumed + bool moreData = false; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (ctx->_channels[ch].active && + ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { + moreData = true; + break; + } + } + + if (!moreData) { + ctx->_state = DriverState::SendingLast; + } + + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + + } else if (ctx->_state == DriverState::SendingLast) { + // Fill completed buffer with zeros (reset signal) + memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + ctx->_state = DriverState::WaitingReset; + + } else { + // WaitingReset complete - stop DMA LCD_CAM.lcd_user.lcd_start = 0; + gdma_stop(ctx->_dmaChannel); ctx->_state = DriverState::Idle; - ctx->_txState = TxState::Idle; - return true; } - - // Refill the completed buffer for next round - ctx->fillBufferForState(completedBuf); - - // Ensure DMA descriptor is ready (owner = DMA) - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - + return true; } void LcdBusContext::printDebugStats() { - LCD_LOG("Stats: ISR=%u, dataFills=%u, zeroFills=%u", - s_isrCount, s_dataFills, s_zeroFills); + LCD_LOG("state=%u, channels=%u, mask=0x%02X", (unsigned)_state, _channelCount, _channelMask); } // ============================================ @@ -1637,8 +1499,6 @@ LcdBus::~LcdBus() { bool LcdBus::begin() { if (_initialized) return true; - LCD_LOG("LcdBus::begin() pin=%d", _pin); - _ctx = LcdBusContext::get(); if (!_ctx) return false; @@ -1656,7 +1516,7 @@ bool LcdBus::begin() { } _initialized = true; - LCD_LOG("LcdBus ready: channel=%d", _channelIdx); + LCD_LOG("LcdBus: ch=%d pin=%d", _channelIdx, _pin); return true; } @@ -1696,40 +1556,34 @@ bool LcdBus::allocateBuffer(uint16_t numPixels) { } bool LcdBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!_initialized || !_ctx || !pixels || numPixels == 0) { - return false; - } + // Always use internal pixel buffer (WLED always calls show() without args) + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - // Wait for idle - uint32_t timeout = 1000; // Longer timeout for large strips + // Wait for previous transmission to complete uint32_t start = millis(); - while (! _ctx->isIdle()) { - if (millis() - start > timeout) { - LCD_LOG("ERROR: Timeout waiting for idle"); + while (!_ctx->isIdle()) { + if (millis() - start > 1000) { _ctx->forceIdle(); break; } delay(1); } - if (!allocateBuffer(numPixels)) return false; + if (!allocateBuffer(_numPixels)) return false; // Encode pixels to byte stream ColorEncoder encoder(_order); uint8_t* dst = _encodeBuffer; uint8_t numCh = encoder.getNumChannels(); - for (uint16_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); dst += numCh; } + _encodedLen = _numPixels * numCh; - _encodedLen = numPixels * numCh; - - // Set data for our channel + // Set data for our channel and start transmission _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); - - // Start transmission return _ctx->startTransmit(); } @@ -1740,21 +1594,14 @@ bool LcdBus::canShow() const { void LcdBus::waitComplete() { if (!_ctx) return; - uint32_t start = millis(); while (!_ctx->isIdle()) { if (millis() - start > 1000) { - LCD_LOG("waitComplete timeout"); - _ctx->printDebugStats(); _ctx->forceIdle(); return; } delay(1); } - -#if WLEDPB_LCD_DEBUG - _ctx->printDebugStats(); -#endif } void LcdBus::setColorOrder(ColorOrder order) { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 9dd93ce260..8072106bd4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -514,27 +514,15 @@ class I2sBus : public IBus { #include "soc/gpio_sig_map.h" #ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE 2048 +#define WLEDPB_LCD_DMA_BUFFER_SIZE 1024 //2048 -> 2048 works well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) #endif #ifndef WLEDPB_LCD_CADENCE_STEPS #define WLEDPB_LCD_CADENCE_STEPS 4 #endif -#ifndef WLEDPB_LCD_DEBUG -#define WLEDPB_LCD_DEBUG 1 -#endif - #define WLEDPB_LCD_MAX_CHANNELS 8 -// Transmission state machine -enum class TxState : uint8_t { - Idle = 0, // Not transmitting - SendingData, // Sending pixel data - SendingReset, // Sending zeros for reset period - StopPending // Will stop on next EOF -}; - class LcdBusContext { public: static LcdBusContext* get(); @@ -548,12 +536,12 @@ class LcdBusContext { uint8_t getChannelCount() const { return _channelCount; } bool startTransmit(); - bool isIdle() const { return _state == DriverState:: Idle; } + bool isIdle() const { return _state == DriverState::Idle; } void forceIdle() { LCD_CAM.lcd_user.lcd_start = 0; + if (_dmaChannel) gdma_stop(_dmaChannel); _state = DriverState::Idle; - _txState = TxState:: Idle; } void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); @@ -563,16 +551,14 @@ class LcdBusContext { LcdBusContext(); ~LcdBusContext(); - size_t IRAM_ATTR encodeIntoBuffer(uint8_t* dest, size_t destLen); - bool IRAM_ATTR hasMoreData() const; - void IRAM_ATTR fillBufferForState(uint8_t bufIdx); + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + void fillBuffer(uint8_t bufIdx); static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); volatile DriverState _state; - volatile TxState _txState; bool _initialized; bool _use16Bit; @@ -584,8 +570,7 @@ class LcdBusContext { // Timing LedTiming _timing; - size_t _resetBytesPerBuffer; - volatile size_t _resetBytesRemaining; + size_t _bufferSize; // Channels struct ChannelData { From 1522d045a870ed3c876536b36259327487eaf7a4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 10:51:34 +0100 Subject: [PATCH 027/173] adding 1MHz WS281x_fast timing --- wled00/bus_manager.cpp | 1 + wled00/bus_wrapper.h | 1 + wled00/src/WLEDpixelBus/WLEDpixelBus.h | 11 +++++++++++ wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h | 2 ++ 4 files changed, 15 insertions(+) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 9a7ed79c04..2b3cb91ed8 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -370,6 +370,7 @@ void BusDigital::setColorOrder(uint8_t colorOrder) { std::vector BusDigital::getLEDTypes() { return { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, + {TYPE_WS281X_FAST, "D", PSTR("WS281x Fast")}, {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, {TYPE_TM1814, "D", PSTR("TM1814")}, {TYPE_TM1815, "D", PSTR("TM1815")}, diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 70b5e53f81..f60f124a2c 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -28,6 +28,7 @@ inline ProtocolDef getProtocol(uint8_t wledType) { case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: return { WLEDpixelBus::Timing::WS2812, 3, false }; + case TYPE_WS281X_FAST: return { WLEDpixelBus::Timing::WS281x_FAST, 3, false }; case TYPE_WS2811_400KHZ:return { WLEDpixelBus::Timing::Generic400Kbps, 3, false }; case TYPE_TM1829: return { WLEDpixelBus::Timing::TM1829, 3, false }; case TYPE_UCS8903: return { WLEDpixelBus::Timing::UCS8903, 3, true }; // 16bit diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 8072106bd4..b349ab3c99 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -521,6 +521,17 @@ class I2sBus : public IBus { #define WLEDPB_LCD_CADENCE_STEPS 4 #endif +#if WLEDPB_LCD_DEBUG + #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) +#else + #define LCD_LOG(fmt, ...) +#endif + +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); +static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); + #define WLEDPB_LCD_MAX_CHANNELS 8 class LcdBusContext { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 57d34ec975..6123dc0019 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -33,6 +33,8 @@ namespace Timing { constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; // WS2805 RGBCW (5ch) constexpr LedTiming SK6812 {300, 900, 800, 450, 200}; // SK6812 / SK6812 RGBW + // ---- High-speed WS281x (1MHz) ---- + constexpr LedTiming WS281x_FAST {330, 670, 660, 340, 100}; // WS281x_FAST: 1MHz, 330ns low, 660ns high, 1000ns period // ---- Titan Micro LEDs ---- constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; // TM1814 RGBW, requires prefix constexpr LedTiming TM1815 {740, 1780, 1440, 1060, 200}; // TM1815 RGBW, requires prefix From eb8c4057ac8cf2f3e7dbc3287cdb7d19b66ce41e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 11:34:03 +0100 Subject: [PATCH 028/173] refactoring: move driver types to individual files --- wled00/bus_wrapper.h | 4 + wled00/const.h | 1 + wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 2168 +---------------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 471 +--- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 666 +++++ wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 147 ++ wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 583 +++++ wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 150 ++ .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 629 +++++ .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 125 + wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 297 +++ wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 72 + 12 files changed, 2684 insertions(+), 2629 deletions(-) create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp create mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index f60f124a2c..cb6750a229 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -5,6 +5,10 @@ //#define NPB_CONF_4STEP_CADENCE #include "src/WLEDpixelBus/WLEDpixelBus.h" #include "src/WLEDpixelBus/WLEDpixelBus_SPI.h" +#include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" +#include "src/WLEDpixelBus/WLEDpixelBus_I2S.h" +#include "src/WLEDpixelBus/WLEDpixelBus_LCD.h" +#include "src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h" //Hardware SPI Pins #define P_8266_HS_MOSI 13 diff --git a/wled00/const.h b/wled00/const.h index 7a6f736cf1..295e7872ae 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -320,6 +320,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define TYPE_TM1914 33 //RGB #define TYPE_SM16825 34 //RGB + WW + CW #define TYPE_TM1815 35 //RGBW (half speed TM1814) +#define TYPE_WS281X_FAST 36 //high-speed WS281x (1MHz) #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/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index bf9fb0f812..a9e08c0ebb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -15,15 +15,10 @@ by @dedehai, 2026 -------------------------------------------------------------------------*/ #include "WLEDpixelBus.h" - -#ifdef WLEDPB_SPI_SUPPORT -#undef FLAG_ATTR -#define FLAG_ATTR(TYPE) -#include "hal/spi_ll.h" -#include "driver/periph_ctrl.h" -#include "esp_private/gdma.h" -#include "esp_rom_gpio.h" -#endif +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_LCD.h" +#include "WLEDpixelBus_ParallelSpi.h" namespace WLEDpixelBus { @@ -68,2161 +63,6 @@ void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) con } } -//============================================================================== -// RMT Bus Implementation -//============================================================================== - -// Context for RMT translate callback - must be in DRAM for IRAM ISR access -static DRAM_ATTR struct { - uint32_t bit0; - uint32_t bit1; - uint16_t resetDuration; -} s_rmtCtx; - -// Static auto-channel counter for RmtBus -uint8_t RmtBus::s_nextAutoChannel = 0; -uint8_t RmtBus::s_activeChannelMask = 0; - -RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) - : _pin(pin) - , _channel(channel) - , _timing(timing) - , _order(order) - , _inverted(false) - , _initialized(false) - , _rmtChannel(RMT_CHANNEL_0) - , _rmtBit0(0) - , _rmtBit1(0) - , _rmtResetTicks(0) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) -{ -} - -RmtBus::~RmtBus() { - end(); -} - -void RmtBus::updateRmtTiming() { - // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick - const uint8_t clockDiv = 2; - 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; - }; - - uint16_t t0h = nsToTicks(_timing.t0h_ns); - uint16_t t0l = nsToTicks(_timing.t0l_ns); - uint16_t t1h = nsToTicks(_timing.t1h_ns); - uint16_t t1l = nsToTicks(_timing.t1l_ns); - - rmt_item32_t bit0, bit1; - - if (_inverted) { - bit0.level0 = 0; bit0.duration0 = t0h; - bit0.level1 = 1; bit0.duration1 = t0l; - bit1.level0 = 0; bit1.duration0 = t1h; - bit1.level1 = 1; bit1.duration1 = t1l; - } else { - bit0.level0 = 1; bit0.duration0 = t0h; - bit0.level1 = 0; bit0.duration1 = t0l; - bit1.level0 = 1; bit1.duration0 = t1h; - bit1.level1 = 0; bit1.duration1 = t1l; - } - - _rmtBit0 = bit0.val; - _rmtBit1 = bit1.val; - _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); -} - -bool RmtBus::begin() { - if (_initialized) return true; - - // Auto-select channel if needed - if (_channel < 0) { - // Use static counter for auto-allocation (fallback if caller didn't specify) - if (s_nextAutoChannel >= getRmtMaxChannels()) { - return false; - } - _channel = s_nextAutoChannel++; - } - - if (_channel >= (int8_t)getRmtMaxChannels()) { - Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, getRmtMaxChannels()); - return false; - } - _rmtChannel = (rmt_channel_t)_channel; - - updateRmtTiming(); - - rmt_config_t config = {}; - - config.rmt_mode = RMT_MODE_TX; - config.channel = _rmtChannel; - config.gpio_num = (gpio_num_t)_pin; - config.mem_block_num = 1; - config.clk_div = 2; // 40MHz - - config.tx_config.loop_en = false; - config.tx_config.carrier_en = false; - //config.tx_config.carrier_freq_hz = 38000; - //config.tx_config.carrier_duty_percent = 33; - config.tx_config.idle_output_en = true; - config.tx_config.idle_level = _inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW; - - - esp_err_t err = rmt_config(&config); - if (err != ESP_OK) { - return false; - } - - // Prioritize RMT over I2S/SPI DMA interrupts (which use LEVEL1) to prevent starvation. - // Try LEVEL3 first, fallback to LEVEL2, then LEVEL1. - int flags = ESP_INTR_FLAG_IRAM; -#ifdef ESP_INTR_FLAG_LEVEL3 - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL3); - if (err != ESP_OK) -#endif - { -#ifdef ESP_INTR_FLAG_LEVEL2 - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL2); - if (err != ESP_OK) -#endif - { - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL1); - } - } - if (err != ESP_OK) { - return false; - } - - err = rmt_translator_init(_rmtChannel, translateCB); - if (err != ESP_OK) { - rmt_driver_uninstall(_rmtChannel); - return false; - } - - _initialized = true; - s_activeChannelMask |= (1 << _channel); - return true; -} - -void RmtBus::end() { - if (!_initialized) return; - - s_activeChannelMask &= ~(1 << _channel); - rmt_driver_uninstall(_rmtChannel); - - if (_encodeBuffer) { - free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } - - _initialized = false; -} - -bool RmtBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) { - return true; - } - - if (_encodeBuffer) { - free(_encodeBuffer); - } - - _encodeBuffer = (uint8_t*)malloc(needed); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; - return true; -} - -bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!pixels) { - pixels = _pixelData; - numPixels = _numPixels; - cct = _cctData; - } - if (numPixels == 0) numPixels = _numPixels; - if (!cct) cct = _cctData; - - if (!_initialized || !pixels || numPixels == 0) { - return false; - } - - // Wait for previous transmission on THIS channel to complete - rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); - - if (!allocateBuffer(numPixels)) return false; - - // Encode pixels to byte stream - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); - - for (uint16_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); - dst += numCh; - } - - // Update context for ISR - s_rmtCtx.bit0 = _rmtBit0; - s_rmtCtx.bit1 = _rmtBit1; - s_rmtCtx.resetDuration = _rmtResetTicks; - - // Start transmission - size_t dataLen = numPixels * numCh; - esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); - - return err == ESP_OK; -} - -bool RmtBus::canShow() const { - if (!_initialized) return true; - // Use rmt_wait_tx_done with 0 timeout to check if TX is done (matching NeoPixelBus) - return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); -} - -void RmtBus::waitComplete() { - if (_initialized) { - rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); - } -} - -void RmtBus::setTiming(const LedTiming& timing) { - _timing = timing; - if (_initialized) { - updateRmtTiming(); - } -} - -void RmtBus::setColorOrder(ColorOrder order) { - _order = order; -} - -void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num) { - if (src == nullptr || dest == nullptr) { - *translated_size = 0; - *item_num = 0; - return; - } - - const uint8_t* psrc = (const uint8_t*)src; - rmt_item32_t* pdest = dest; - size_t size = 0; - size_t num = 0; - - uint32_t bit0 = s_rmtCtx.bit0; - uint32_t bit1 = s_rmtCtx.bit1; - uint16_t resetDuration = s_rmtCtx.resetDuration; - - // Each byte produces 8 RMT items - for (;;) - { - uint8_t data = *psrc; - - for (uint8_t bit = 0; bit < 8; bit++) - { - pdest->val = (data & 0x80) ? bit1 : bit0; - pdest++; - data <<= 1; - } - num += 8; - size++; - - // If this is the last byte, extend the last bit's LOW duration - // to include the full reset signal length - if (size >= src_size) - { - pdest--; - pdest->duration1 = resetDuration; - break; - } - - if (num >= wanted_num) - { - break; - } - - psrc++; - } - - *translated_size = size; - *item_num = num; -} - -//============================================================================== -// I2S Bus Implementation -//============================================================================== - -#ifdef WLEDPB_I2S_SUPPORT - -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); - } - _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; - } -} - -I2sBusContext::I2sBusContext(uint8_t busNum) - : _busNum(busNum) - , _state(DriverState::Idle) - , _initialized(false) - , _bufferSize(0) - , _activeBuffer(0) - , _timing{0, 0, 0, 0, 0} - , _clockDiv(1) - , _isrHandle(nullptr) - , _channelCount(0) - , _channelMask(0) - , _stagedMask(0) - , _maxDataLen(0) -{ -#if defined(WLEDPB_ESP32) - _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; -#else - _i2sDev = &I2S0; -#endif - - for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; - } - - _dmaDesc[0] = _dmaDesc[1] = nullptr; - _dmaBuffer[0] = _dmaBuffer[1] = nullptr; -} - -I2sBusContext:: ~I2sBusContext() { - deinit(); -} - -bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { - if (_initialized) return true; - - _timing = timing; - _bufferSize = (bufferSize + 3) & ~3; // align to 4 bytes - - // Allocate DMA buffers (4-byte aligned for DMA) - for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - log_e("I2S DMA buffer alloc failed"); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, _bufferSize); - - _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - log_e("I2S DMA desc alloc failed"); - deinit(); - return false; - } - } - - // Setup DMA descriptors - circular chain for gapless ping-pong - for (int i = 0; i < 2; i++) { - _dmaDesc[i]->size = _bufferSize; - _dmaDesc[i]->length = _bufferSize; - _dmaDesc[i]->buf = _dmaBuffer[i]; - _dmaDesc[i]->eof = 1; // Generate interrupt on completion - _dmaDesc[i]->sosf = 0; - _dmaDesc[i]->owner = 1; - } - _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; - _dmaDesc[1]->qe.stqe_next = _dmaDesc[0]; - - // Enable I2S peripheral -#if defined(WLEDPB_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 8-bit parallel LCD mode (matching NeoPixelBus) - _i2sDev->conf2.val = 0; - _i2sDev->conf2.lcd_en = 1; - _i2sDev->conf2.lcd_tx_wrx2_en = 1; // Required for 8-bit parallel output - _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(WLEDPB_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 = 1; // 16-bit single channel - _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold - - // PCM bypass - _i2sDev->conf1.val = 0; - _i2sDev->conf1.tx_stop_en = 0; - _i2sDev->conf1.tx_pcm_bypass = 1; - - // Channel config - _i2sDev->conf_chan.val = 0; - _i2sDev->conf_chan.tx_chan_mod = 1; // Right channel - - // I2S conf - _i2sDev->conf.val = 0; - _i2sDev->conf.tx_msb_shift = 0; // No shift in parallel mode - _i2sDev->conf.tx_right_first = 1; - - // Clear timing register - _i2sDev->timing.val = 0; - - // Calculate clock divider for 4-step cadence (matching NeoPixelBus) - // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) - // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns - // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) - const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 - uint32_t bitPeriodNs = timing.bitPeriod(); - -#if defined(WLEDPB_ESP32) - const double baseClockMhz = 160.0; -#else - const double baseClockMhz = 80.0; -#endif - - // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz - // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 - double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; - if (clkmdiv < 2.0) clkmdiv = 2.0; - if (clkmdiv > 255.0) clkmdiv = 255.0; - - uint8_t clkmInteger = (uint8_t)clkmdiv; - double clkmFraction = clkmdiv - clkmInteger; - - // Convert fraction to divB/divA (fraction = divB/divA) - uint8_t divB = 0; - uint8_t divA = 0; - if (clkmFraction > 0.001) { - divA = 63; // use max denominator for best precision - divB = (uint8_t)(clkmFraction * 63.0 + 0.5); - if (divB >= divA) divB = divA - 1; - } - - _clockDiv = clkmInteger; - - Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", - bitPeriodNs, clkmInteger, divB, divA, bckDiv); - double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; - if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; - Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", - actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); - - // Set clock (with fractional divider for accurate timing) - _i2sDev->clkm_conf.val = 0; - _i2sDev->clkm_conf.clk_en = 1; - _i2sDev->clkm_conf.clkm_div_a = divA; - _i2sDev->clkm_conf.clkm_div_b = divB; - _i2sDev->clkm_conf.clkm_div_num = clkmInteger; - - // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) - _i2sDev->sample_rate_conf.val = 0; - _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; - _i2sDev->sample_rate_conf.tx_bits_mod = 8; - - // 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(WLEDPB_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_LEVEL1, - dmaISR, this, &_isrHandle); - if (err != ESP_OK) { - log_e("I2S ISR alloc failed: %d", err); - deinit(); - return false; - } - - _initialized = true; - Serial.printf("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); - return true; -} - -void I2sBusContext::deinit() { - if (_i2sDev) { - _i2sDev->int_ena.val = 0; // Disable interrupts first - _i2sDev->conf.tx_start = 0; - _i2sDev->out_link.start = 0; - } - _state = DriverState::Idle; - - if (_isrHandle) { - esp_intr_free(_isrHandle); - _isrHandle = nullptr; - } - - for (int i = 0; i < 2; i++) { - if (_dmaBuffer[i]) { - heap_caps_free(_dmaBuffer[i]); - _dmaBuffer[i] = nullptr; - } - if (_dmaDesc[i]) { - heap_caps_free(_dmaDesc[i]); - _dmaDesc[i] = nullptr; - } - } - -#if defined(WLEDPB_ESP32) - periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); -#else - periph_module_disable(PERIPH_I2S0_MODULE); -#endif - - _initialized = false; -} - -int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { - // Find free slot - int8_t idx = -1; - for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { - if (! _channels[i].active) { - idx = i; - break; - } - } - - if (idx < 0) return -1; - - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); - - // Configure GPIO - gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); - - // Route I2S output to GPIO - int sigIdx; -#if defined(WLEDPB_ESP32) - sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; -#else - sigIdx = I2S0O_DATA_OUT0_IDX; -#endif - sigIdx += idx; - - gpio_matrix_out(pin, sigIdx, false, false); - - return idx; -} - -void I2sBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; - - if (_channels[channelIdx].pin >= 0) { - gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); - } - - _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); -} - -void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; - - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - _channels[channelIdx].srcPos = 0; - - if (len > _maxDataLen) { - _maxDataLen = len; - } - - // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() - if (_stagedMask & (1 << channelIdx)) { - _stagedMask = 0; - } - _stagedMask |= (1 << channelIdx); -} - -void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { - // 4-step cadence encoding for parallel output - // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) - // Desired output: [HIGH][data][data][LOW] for each bit - // - // ESP32 I2S LCD mode with lcd_tx_wrx2_en=1 swaps half-words within 32-bit values: - // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] - // (NeoPixelBus documents this as "bytes within the words are swapped") - // So we write: p[0]=step2, p[1]=step3, p[2]=step0, p[3]=step1 - // - // Buffer is always filled completely (zeros = LOW = reset signal) - - memset(dest, 0, destLen); - size_t pos = 0; - - // Process each source byte position across all channels - while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte - bool hasData = false; - - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - - hasData = true; - uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; - uint8_t chMask = (1 << ch); - - uint8_t* p = dest + pos; - for (int bit = 7; bit >= 0; bit--) { - uint8_t dataVal = (srcByte >> bit) & 1; - - // Half-word swapped: memory layout [step2, step3, step0, step1] - // Step 0 (HIGH) -> p[2] - p[2] |= chMask; - - // Step 1 (data) -> p[3] - if (dataVal) p[3] |= chMask; - - // Step 2 (data) -> p[0] - if (dataVal) p[0] |= chMask; - - // Step 3 (LOW) -> p[1] (already 0) - - p += 4; - } - } - - if (!hasData) break; - - // Advance all channel positions - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - _channels[ch].srcPos++; - } - } - - pos += 32; - } - // Rest of buffer remains zero (reset signal) from memset -} - -void I2sBusContext::fillBuffer(uint8_t bufIdx) { - encode4Step(_dmaBuffer[bufIdx], _bufferSize); - // desc->length stays at _bufferSize (set in init, never changes) -} - -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 false; - _stagedMask = 0; // Reset for next frame - - _maxDataLen = 0; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active) { - _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) { - _maxDataLen = _channels[ch].srcLen; - } - } - } - - // Fill both buffers initially - fillBuffer(0); - fillBuffer(1); - - // Restore DMA descriptor ownership (DMA clears owner to 0 after processing) - _dmaDesc[0]->owner = 1; - _dmaDesc[1]->owner = 1; - - _activeBuffer = 0; - _state = DriverState::Sending; - - // Reset DMA and FIFO before starting - _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; - - // Clear and enable interrupts - _i2sDev->int_clr.val = 0xFFFFFFFF; - _i2sDev->int_ena.out_eof = 1; - - // Enable DMA and start - _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; - - return true; -} - -static volatile uint32_t s_i2sIsrCount = 0; -static volatile uint32_t s_i2sIsrSending = 0; -static volatile uint32_t s_i2sIsrReset = 0; -static volatile uint32_t s_i2sIsrIdle = 0; - -void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { - I2sBusContext* ctx = (I2sBusContext*)arg; - i2s_dev_t* dev = ctx->_i2sDev; - - uint32_t status = dev->int_st.val; - dev->int_clr.val = status; - - if (!(status & I2S_OUT_EOF_INT_ST)) return; - - s_i2sIsrCount++; - - // The completed buffer just finished playing; DMA is now on the other buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer ^= 1; - - if (ctx->_state == DriverState::Sending) { - s_i2sIsrSending++; - // Encode next chunk into the completed buffer - // encode4Step always fills the full buffer (zeros for any remainder) - ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); - - // Check if all source data has been consumed - bool moreData = false; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && - ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { - moreData = true; - break; - } - } - if (!moreData) { - // Last data chunk was just encoded into completedBuf. - // DMA is currently playing the OTHER buffer. We need to wait - // for that to finish so the last-data buffer gets played. - ctx->_state = DriverState::SendingLast; - } - - // Restore DMA ownership so hardware can replay this buffer - ctx->_dmaDesc[completedBuf]->owner = 1; - } else if (ctx->_state == DriverState::SendingLast) { - // The other buffer just finished. DMA is now playing the last-data buffer. - // Fill completed buffer with zeros (reset signal) so it plays after. - memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); - ctx->_dmaDesc[completedBuf]->owner = 1; - s_i2sIsrReset++; - ctx->_state = DriverState::WaitingReset; - } else { - // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. - s_i2sIsrIdle++; - dev->int_ena.out_eof = 0; - dev->conf.tx_start = 0; - dev->out_link.start = 0; - ctx->_state = DriverState::Idle; - } -} - -// I2sBus implementation - -I2sBus::I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, - uint8_t busNum, size_t bufferSize) - : _pin(pin) - , _busNum(busNum) - , _bufferSize(bufferSize) - , _timing(timing) - , _order(order) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) -{ -} - -I2sBus::~I2sBus() { - end(); -} - -bool I2sBus::begin() { - if (_initialized) return true; - - _ctx = I2sBusContext::get(_busNum); - if (!_ctx) return false; - - if (!_ctx->init(_timing, _bufferSize)) { - I2sBusContext::release(_busNum); - _ctx = nullptr; - return false; - } - - _channelIdx = _ctx->registerChannel(_pin, this); - if (_channelIdx < 0) { - Serial.printf("[I2S] registerChannel failed for pin %d\n", _pin); - I2sBusContext::release(_busNum); - _ctx = nullptr; - return false; - } - - _initialized = true; - Serial.printf("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n", _pin, _busNum, _channelIdx); - return true; -} - -void I2sBus::end() { - if (!_initialized) return; - - if (_ctx) { - // Wait for any active transmission to complete before cleanup - while (!_ctx->isIdle()) vTaskDelay(1); - _ctx->unregisterChannel(_channelIdx); - I2sBusContext::release(_busNum); - _ctx = nullptr; - } - - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } - - _initialized = false; -} - -bool I2sBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) { - return true; - } - - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - } - - _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; - return true; -} - -bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - // Always use internal pixel buffer (WLED always calls show() without args) - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - - // Wait for previous transmission to complete - while (!_ctx->isIdle()) { - vTaskDelay(1); - } - - if (!allocateBuffer(_numPixels)) return false; - - // Encode pixels to byte stream - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); - - for (uint16_t i = 0; i < _numPixels; i++) { - encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); - dst += numCh; - } - - // Set data for our channel and start - _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); - return _ctx->startTransmit(); -} - -bool I2sBus::canShow() const { - if (!_ctx) return true; - return _ctx->isIdle(); -} - -void I2sBus::waitComplete() { - while (_ctx && !_ctx->isIdle()) { - vTaskDelay(1); - } -} - -void I2sBus::setColorOrder(ColorOrder order) { - _order = order; -} - -#endif // WLEDPB_I2S_SUPPORT - -//============================================================================== -// LCD Bus Implementation (ESP32-S3) -//============================================================================== -/*------------------------------------------------------------------------- -WLEDpixelBus - LCD Implementation with Continuous DMA (Glitch-Free) - -Key design: -- DMA runs continuously in circular mode (buf0 -> buf1 -> buf0 -> ...) -- Buffers are filled with data or zeros (for reset period) -- Stop only after reset period completes on buffer boundary -- No DMA reconfiguration during transmission --------------------------------------------------------------------------*/ - -#ifdef WLEDPB_LCD_SUPPORT - -#include "driver/periph_ctrl.h" // works on S3 -//#include "esp_private/periph_ctrl.h" // works on C3, todo: use ifdefs -#include "esp_private/gdma.h" -#include "esp_rom_gpio.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" - -// ============================================ -// Configuration -// ============================================ - -#if WLEDPB_LCD_DEBUG - #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) -#else - #define LCD_LOG(fmt, ...) -#endif - -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); -static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); - - - - -LcdBusContext* LcdBusContext::_instance = nullptr; -uint8_t LcdBusContext::_refCount = 0; - -LcdBusContext* LcdBusContext::get() { - if (_instance == nullptr) { - _instance = new LcdBusContext(); - } - _refCount++; - return _instance; -} - -void LcdBusContext::release() { - if (_refCount == 0) return; - _refCount--; - if (_refCount == 0 && _instance) { - delete _instance; - _instance = nullptr; - } -} - -LcdBusContext::LcdBusContext() - : _state(DriverState::Idle) - , _initialized(false) - , _use16Bit(false) - , _dmaChannel(nullptr) - , _timing{0, 0, 0, 0, 0} - , _bufferSize(WLEDPB_LCD_DMA_BUFFER_SIZE) - , _channelCount(0) - , _channelMask(0) - , _stagedMask(0) - , _maxDataLen(0) - , _activeBuffer(0) -{ - for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; - } - for (int i = 0; i < 2; i++) { - _dmaDesc[i] = nullptr; - _dmaBuffer[i] = nullptr; - } -} - -LcdBusContext::~LcdBusContext() { - deinit(); -} - -bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16Bit) { - if (_initialized) return true; - - LCD_LOG("Init: buf=%u x2, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", - WLEDPB_LCD_DMA_BUFFER_SIZE, WLEDPB_LCD_CADENCE_STEPS, - timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); - - _timing = timing; - _use16Bit = use16Bit; - _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; - - uint32_t bitPeriodNs = timing.bitPeriod(); - - // Allocate double DMA buffers - for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - LCD_LOG("ERROR: DMA buffer %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); - - _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - LCD_LOG("ERROR: DMA desc %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); - } - - // Setup DMA descriptors in CIRCULAR mode (never changes during operation!) - // buf0 -> buf1 -> buf0 -> buf1 -> ... - for (int i = 0; i < 2; i++) { - _dmaDesc[i]->dw0.size = WLEDPB_LCD_DMA_BUFFER_SIZE; - _dmaDesc[i]->dw0.length = WLEDPB_LCD_DMA_BUFFER_SIZE; - _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR - _dmaDesc[i]->buffer = _dmaBuffer[i]; - _dmaDesc[i]->next = _dmaDesc[i ^ 1]; // Point to other buffer (circular) - } - - - - // 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 - double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; - - LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); - - if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { - LCD_LOG("ERROR: Invalid clock divider"); - deinit(); - 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; - LCD_CAM.lcd_user.lcd_2byte_en = use16Bit ? 1 : 0; - 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) { - LCD_LOG("ERROR: GDMA alloc failed: %d", err); - deinit(); - return false; - } - - err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); - if (err != ESP_OK) { - LCD_LOG("ERROR: GDMA connect failed: %d", err); - deinit(); - return false; - } - - gdma_strategy_config_t strategy_config = { - .owner_check = false, - .auto_update_desc = false - }; - gdma_apply_strategy(_dmaChannel, &strategy_config); - - // Register DMA callback - gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; - gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); - - _initialized = true; - LCD_LOG("Init OK: clkm_div=%u+%u/%u", - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_num, - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_b, - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_a); - return true; -} - -void LcdBusContext::deinit() { - // Stop transmission - LCD_CAM.lcd_user.lcd_start = 0; - _state = DriverState::Idle; - - if (_dmaChannel) { - gdma_stop(_dmaChannel); - gdma_disconnect(_dmaChannel); - gdma_del_channel(_dmaChannel); - _dmaChannel = nullptr; - } - - for (int i = 0; i < 2; i++) { - if (_dmaBuffer[i]) { - heap_caps_free(_dmaBuffer[i]); - _dmaBuffer[i] = nullptr; - } - if (_dmaDesc[i]) { - heap_caps_free(_dmaDesc[i]); - _dmaDesc[i] = nullptr; - } - } - - if (_initialized) { - periph_module_disable(PERIPH_LCD_CAM_MODULE); - } - - _initialized = false; -} - -int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus) { - int8_t idx = -1; - for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - if (!_channels[i].active) { - idx = i; - break; - } - } - if (idx < 0) return -1; - - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); - - esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, false, false); - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); - gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); - - LCD_LOG("Channel %d: pin=%d, mask=0x%02X", idx, pin, _channelMask); - return idx; -} - -void LcdBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; - - if (_channels[channelIdx].pin >= 0) { - gpio_matrix_out(_channels[channelIdx].pin, SIG_GPIO_OUT_IDX, false, false); - pinMode(_channels[channelIdx].pin, INPUT); - } - - _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); -} - -void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; - - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - _channels[channelIdx].srcPos = 0; - - if (len > _maxDataLen) _maxDataLen = len; - - if (_stagedMask & (1 << channelIdx)) _stagedMask = 0; - _stagedMask |= (1 << channelIdx); -} - -void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { - // 4-step cadence encoding for parallel output without byte swapping - // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) - // Desired output: [HIGH][data][data][LOW] for each bit - // Buffer is always filled completely (zeros = LOW = reset signal) - - memset(dest, 0, destLen); - size_t pos = 0; - - // Process each source byte position across all channels - while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte - bool hasData = false; - - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - - hasData = true; - uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; - uint8_t chMask = (1 << ch); - - uint8_t* p = dest + pos; - for (int bit = 7; bit >= 0; bit--) { - uint8_t dataVal = (srcByte >> bit) & 1; - - // Step 0 (HIGH) - p[0] |= chMask; - // Step 1 (data) - if (dataVal) p[1] |= chMask; - // Step 2 (data) - if (dataVal) p[2] |= chMask; - // Step 3 (LOW) - already 0 - p += 4; - } - } - - if (!hasData) break; - - // Advance all channel positions - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - _channels[ch].srcPos++; - } - } - - pos += 32; - } - // Rest of buffer remains zero (reset signal) from memset -} - -void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { - encode4Step(_dmaBuffer[bufIdx], _bufferSize); -} - -bool LcdBusContext::startTransmit() { - if (_stagedMask != _channelMask) return false; // wait for all channels - _stagedMask = 0; - - if (_state != DriverState::Idle) return false; - if (_channelCount == 0) return false; - - // Reset channel positions - _maxDataLen = 0; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) { - _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) _maxDataLen = _channels[ch].srcLen; - } - } - if (_maxDataLen == 0) return false; - - // Fill both buffers initially - fillBuffer(0); - fillBuffer(1); - - _dmaDesc[0]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _dmaDesc[1]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - - _activeBuffer = 0; - _state = DriverState::Sending; - - // Start DMA (circular mode - descriptors already linked) - 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; - - gdma_start(_dmaChannel, (intptr_t)_dmaDesc[0]); - esp_rom_delay_us(1); - LCD_CAM.lcd_user.lcd_start = 1; - - return true; -} - -IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, - gdma_event_data_t* event_data, - void* user_data) { - LcdBusContext* ctx = (LcdBusContext*)user_data; - - // The completed buffer just finished playing; DMA is now on the other buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer ^= 1; - - if (ctx->_state == DriverState::Sending) { - // Encode next chunk into the completed buffer - ctx->fillBuffer(completedBuf); - - // Check if all source data has been consumed - bool moreData = false; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && - ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { - moreData = true; - break; - } - } - - if (!moreData) { - ctx->_state = DriverState::SendingLast; - } - - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - - } else if (ctx->_state == DriverState::SendingLast) { - // Fill completed buffer with zeros (reset signal) - memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - ctx->_state = DriverState::WaitingReset; - - } else { - // WaitingReset complete - stop DMA - LCD_CAM.lcd_user.lcd_start = 0; - gdma_stop(ctx->_dmaChannel); - ctx->_state = DriverState::Idle; - } - - return true; -} - -void LcdBusContext::printDebugStats() { - LCD_LOG("state=%u, channels=%u, mask=0x%02X", (unsigned)_state, _channelCount, _channelMask); -} - -// ============================================ -// LcdBus Implementation -// ============================================ - -LcdBus::LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, - size_t bufferSize, bool use16Bit) - : _pin(pin) - , _bufferSize(bufferSize) - , _use16Bit(use16Bit) - , _timing(timing) - , _order(order) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) - , _encodedLen(0) -{ -} - -LcdBus::~LcdBus() { - end(); -} - -bool LcdBus::begin() { - if (_initialized) return true; - - _ctx = LcdBusContext::get(); - if (!_ctx) return false; - - if (!_ctx->init(_timing, _bufferSize, _use16Bit)) { - LcdBusContext::release(); - _ctx = nullptr; - return false; - } - - _channelIdx = _ctx->registerChannel(_pin, this); - if (_channelIdx < 0) { - LcdBusContext::release(); - _ctx = nullptr; - return false; - } - - _initialized = true; - LCD_LOG("LcdBus: ch=%d pin=%d", _channelIdx, _pin); - return true; -} - -void LcdBus::end() { - if (!_initialized) return; - - if (_ctx) { - _ctx->unregisterChannel(_channelIdx); - LcdBusContext::release(); - _ctx = nullptr; - } - - if (_encodeBuffer) { - free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } - - _initialized = false; -} - -bool LcdBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) { - return true; - } - - if (_encodeBuffer) free(_encodeBuffer); - - _encodeBuffer = (uint8_t*)malloc(needed); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; - return true; -} - -bool LcdBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - // Always use internal pixel buffer (WLED always calls show() without args) - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - - // Wait for previous transmission to complete - uint32_t start = millis(); - while (!_ctx->isIdle()) { - if (millis() - start > 1000) { - _ctx->forceIdle(); - break; - } - delay(1); - } - - if (!allocateBuffer(_numPixels)) return false; - - // Encode pixels to byte stream - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); - - for (uint16_t i = 0; i < _numPixels; i++) { - encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); - dst += numCh; - } - _encodedLen = _numPixels * numCh; - - // Set data for our channel and start transmission - _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); - return _ctx->startTransmit(); -} - -bool LcdBus::canShow() const { - if (!_ctx) return true; - return _ctx->isIdle(); -} - -void LcdBus::waitComplete() { - if (!_ctx) return; - uint32_t start = millis(); - while (!_ctx->isIdle()) { - if (millis() - start > 1000) { - _ctx->forceIdle(); - return; - } - delay(1); - } -} - -void LcdBus::setColorOrder(ColorOrder order) { - _order = order; -} - -#endif // WLEDPB_LCD_SUPPORT - -//============================================================================== -// SPI Parallel Bus Implementation (ESP32-C3) -//============================================================================== - -#ifdef WLEDPB_SPI_SUPPORT - -// low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) -static inline void spi_ll_apply_config(spi_dev_t *hw) -{ - hw->cmd.update = 1; - while (hw->cmd.update); //waiting config applied -} - -static inline void spi_ll_user_start(spi_dev_t *hw) -{ - hw->cmd.usr = 1; -} - - -// Pin assignments for SPI2 quad mode on C3 -// SPI2 signals: FSPID (MOSI/D0), FSPIQ (MISO/D1), FSPIWP (D2), FSPIHD (D3) -static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_OUT_IDX, FSPIHD_OUT_IDX }; - -// Encoding patterns for SPI quad mode (4-step cadence, LSB first) -// Each lane is one bit position in a nibble, one byte = two clock cycles, 2 bytes = one 4-step bit -static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high -static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high - -SpiBusContext* SpiBusContext::_instance = nullptr; -uint8_t SpiBusContext::_refCount = 0; - -SpiBusContext* SpiBusContext::get() { - if (_instance == nullptr) { - _instance = new SpiBusContext(); - } - _refCount++; - return _instance; -} - -void SpiBusContext::release() { - if (_refCount == 0) return; - _refCount--; - if (_refCount == 0 && _instance) { - delete _instance; - _instance = nullptr; - } -} - -SpiBusContext::SpiBusContext() - : _sending(false) - , _initialized(false) - , _hasStarted(false) - , _currentBuffer(0) - , _isrHandle(nullptr) - , _spiIsrHandle(nullptr) - , _hw(&GPSPI2) - , _channelCount(0) - , _framePos(0) - , _numBytes(0) - , _lastTransmitMs(0) - , _stagedMask(0) - , _channelMask(0) -{ - _dmaBuffer[0] = _dmaBuffer[1] = nullptr; - for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, false}; - } -} - -SpiBusContext::~SpiBusContext() { - deinit(); -} - -bool SpiBusContext::isSpiDone() { - if (!_hasStarted) return true; // no transfer ever started - - // We are logically done once all real data is encoded - if (!_sending) { - return true; - } - - // Fail-safe watchdog: if stuck for > 250ms, recover it - if (millis() - _lastTransmitMs > 250) { - forceIdle(); - return true; - } - - return false; -} - -void SpiBusContext::forceIdle() { - _sending = false; - _stagedMask = 0; - - // Stop DMA immediately - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - - if (_hw) { - _hw->cmd.usr = 0; - _hw->dma_int_clr.val = 0xFFFFFFFF; - } -} - -void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { - uint8_t* dst = _dmaBuffer[bufIdx]; - memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); - - if (!_sending) { - return; - } - - size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte - size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; - size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; - - if (srcThisChunk == 0) { - _sending = false; - _framePos = 0; - return; - } - - for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { - if (!_channels[lane].active || !_channels[lane].srcData) continue; - - size_t srcLen = _channels[lane].srcLen; - if (_framePos >= srcLen) continue; // Past the end of this lane's data, leave buffer 0 (low/no pulse) - - size_t validBytes = srcLen - _framePos; - if (validBytes > srcThisChunk) validBytes = srcThisChunk; - - const uint16_t zerobit = SPI_ZERO_BIT << lane; - const uint16_t onebit = SPI_ONE_BIT << lane; - const uint8_t* src = _channels[lane].srcData; - uint16_t* pOut = reinterpret_cast(dst); - - for (size_t i = 0; i < validBytes; i++) { - uint8_t v = src[_framePos + i]; - *pOut++ |= (v & 0x80) ? onebit : zerobit; - *pOut++ |= (v & 0x40) ? onebit : zerobit; - *pOut++ |= (v & 0x20) ? onebit : zerobit; - *pOut++ |= (v & 0x10) ? onebit : zerobit; - *pOut++ |= (v & 0x08) ? onebit : zerobit; - *pOut++ |= (v & 0x04) ? onebit : zerobit; - *pOut++ |= (v & 0x02) ? onebit : zerobit; - *pOut++ |= (v & 0x01) ? onebit : zerobit; - } - } - _framePos += srcThisChunk; -} - -// SPI ISR: handles trans_done (restart SPI bit counter) and outfifo_empty_err -// (FIFO underrun recovery). outfifo_empty_err is temporarily disabled after -// handling to prevent infinite ISR loops; trans_done re-enables it. -void IRAM_ATTR SpiBusContext::spiISR(void* arg) { - SpiBusContext* ctx = (SpiBusContext*)arg; - uint32_t raw = ctx->_hw->dma_int_raw.val; - - if (raw & 0x02) { - // outfifo_empty_err: FIFO starved, disable to prevent ISR loop - ctx->_hw->dma_int_ena.outfifo_empty_err = 0; - ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags - ctx->_hw->cmd.usr = 0; // Stop SPI - - spi_ll_dma_tx_fifo_reset(ctx->_hw); - spi_ll_outfifo_empty_clr(ctx->_hw); - - gdma_dev_t* dma = &GDMA; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - ctx->_dmaDesc[0].owner = 1; - ctx->_dmaDesc[1].owner = 1; - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&ctx->_dmaDesc[ctx->_currentBuffer]); - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - - spi_ll_clear_int_stat(ctx->_hw); - spi_ll_set_mosi_bitlen(ctx->_hw, 253952); - ctx->_hw->cmd.usr = 1; - return; - } - - if (raw & 0x1000) { - ctx->_hw->dma_int_clr.trans_done = 1; - spi_ll_set_mosi_bitlen(ctx->_hw, 253952); - ctx->_hw->cmd.usr = 1; - ctx->_hw->dma_int_ena.outfifo_empty_err = 1; - } -} - -void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { - SpiBusContext* ctx = (SpiBusContext*)arg; - gdma_dev_t* dma = &GDMA; - - if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { - uint8_t completedBuf = ctx->_currentBuffer; - ctx->encodeSpiChunk(completedBuf); - - // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI - ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; - ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; - ctx->_dmaDesc[completedBuf].eof = 1; - ctx->_dmaDesc[completedBuf].owner = 1; - ctx->_currentBuffer = completedBuf ? 0 : 1; - } - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; -} - -bool SpiBusContext::init(const LedTiming& timing) { - if (_initialized) return true; - - // Allocate DMA buffers - for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, - MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); - if (!_dmaBuffer[i]) { - Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, WLEDPB_SPI_DMA_BUFFER_SIZE); - } - - // Setup DMA descriptors - circular linked list - for (int i = 0; i < 2; i++) { - _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; - _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; - _dmaDesc[i].owner = 1; - _dmaDesc[i].sosf = 0; - _dmaDesc[i].eof = 1; - _dmaDesc[i].buf = _dmaBuffer[i]; - } - _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; - _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; - - // Enable peripheral clocks and force-reset (SPI2 + DMA) - // periph_module_enable() uses ref counting and may be a no-op if the - // peripheral was already enabled. Explicit reset ensures clean state. - periph_module_enable(PERIPH_SPI2_MODULE); - periph_module_reset(PERIPH_SPI2_MODULE); - periph_module_enable(PERIPH_GDMA_MODULE); - periph_module_reset(PERIPH_GDMA_MODULE); - - // Configure SPI2 master - spi_ll_master_init(_hw); - spi_ll_master_set_mode(_hw, 0); - spi_ll_set_tx_lsbfirst(_hw, true); - - _hw->user.usr_command = 0; - _hw->user.usr_addr = 0; - _hw->user.usr_dummy = 0; - _hw->user.usr_miso = 0; - _hw->user.usr_mosi = 1; - - // Clear idle output polarities for D2/D3 - _hw->ctrl.q_pol = 0; - _hw->ctrl.d_pol = 0; - _hw->ctrl.hold_pol = 0; - _hw->ctrl.wp_pol = 0; - - spi_line_mode_t linemode = {}; - linemode.data_lines = 4; // quad mode - spi_ll_master_set_line_mode(_hw, linemode); - - // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config - // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) - // Clock: 4 steps per bit → 4 SPI clock cycles per bit period. - // targetFreq = 4 / (bitPeriod_ns * 1e-9) = 4,000,000,000 / bitPeriod_ns - uint32_t bitPeriodNs = timing.bitPeriod(); - uint32_t targetFreq = 4000000000UL / bitPeriodNs; - if (targetFreq < 2000000) targetFreq = 2000000; - if (targetFreq > 5000000) targetFreq = 5000000; - - spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); - - // Route SPI clock to a dummy pin (needed for DMA to work) - pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); - - spi_ll_set_mosi_bitlen(_hw, 16384); - spi_ll_enable_mosi(_hw, true); - - spi_ll_dma_tx_enable(_hw, true); - spi_ll_dma_tx_fifo_reset(_hw); - spi_ll_outfifo_empty_clr(_hw); - spi_ll_apply_config(_hw); - - // Configure GDMA - gdma_dev_t* dma = &GDMA; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - - esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, - gdmaISR, this, &_isrHandle); - if (err != ESP_OK) { - Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); - deinit(); - return false; - } - - // Install SPI ISR for trans_done and outfifo_empty_err recovery - _hw->dma_int_clr.val = 0xFFFFFFFF; - _hw->dma_int_ena.trans_done = 1; - _hw->dma_int_ena.outfifo_empty_err = 1; - err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, - spiISR, this, &_spiIsrHandle); - if (err != ESP_OK) { - Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); - deinit(); - return false; - } - - _initialized = true; - return true; -} - -void SpiBusContext::deinit() { - _sending = false; - - // Stop SPI and DMA before freeing resources - if (_hw) { - _hw->cmd.usr = 0; // Stop SPI transfer - } - - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - - if (_hw) { - _hw->dma_int_ena.trans_done = 0; // Disable SPI trans_done interrupt - } - - if (_spiIsrHandle) { - esp_intr_free(_spiIsrHandle); - _spiIsrHandle = nullptr; - } - - if (_isrHandle) { - esp_intr_free(_isrHandle); - _isrHandle = nullptr; - } - - for (int i = 0; i < 2; i++) { - if (_dmaBuffer[i]) { - heap_caps_free(_dmaBuffer[i]); - _dmaBuffer[i] = nullptr; - } - } - - periph_module_disable(PERIPH_SPI2_MODULE); - periph_module_disable(PERIPH_GDMA_MODULE); - _initialized = false; -} - -int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus) { - int8_t idx = -1; - for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - if (!_channels[i].active) { - idx = i; - break; - } - } - if (idx < 0) return -1; - - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); - - // Route SPI data signal to GPIO - pinMode(pin, OUTPUT); - pinMatrixOutAttach(pin, SPI_SIGNAL_INDICES[idx], false, false); - - return idx; -} - -void SpiBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; - - if (_channels[channelIdx].pin >= 0) { - gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); - } - - _channels[channelIdx] = {nullptr, -1, nullptr, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); -} - -void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - - // Mark this channel as staged - _stagedMask |= (1 << channelIdx); - - if (len > _numBytes) _numBytes = len; -} - -void SpiBusContext::resetAndStart() { - spi_ll_dma_tx_fifo_reset(_hw); - spi_ll_outfifo_empty_clr(_hw); - spi_ll_apply_config(_hw); - - _dmaDesc[0].owner = 1; - _dmaDesc[1].owner = 1; - - gdma_dev_t* dma = &GDMA; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; -} - -bool SpiBusContext::startTransmit() { - if (_sending) return false; - if (_channelCount == 0) return false; - - if (_stagedMask != _channelMask) return false; - _stagedMask = 0; - - _lastTransmitMs = millis(); - size_t newBytes = 0; - for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcLen > newBytes) { - newBytes = _channels[ch].srcLen; - } - } - - if (!_hasStarted) { - _framePos = 0; - _numBytes = newBytes; - - // Max buffer-aligned bit count: continuous output for ~97ms before - // trans_done restarts. outfifo_empty_err handles FIFO underruns. - spi_ll_set_mosi_bitlen(_hw, 253952); - spi_ll_clear_int_stat(_hw); - - _sending = true; - _currentBuffer = 0; - encodeSpiChunk(0); - encodeSpiChunk(1); - - resetAndStart(); - _hasStarted = true; - spi_ll_user_start(_hw); - } else { - // Hardware already running - just update pointers, ISR picks up new data - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - _framePos = 0; - _numBytes = newBytes; - _sending = true; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - } - - return true; -} - -// SpiBus implementation - -ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) - : _pin(pin) - , _timing(timing) - , _order(order) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) -{ -} - -ParallelSpiBus::~ParallelSpiBus() { - end(); -} - -bool ParallelSpiBus::begin() { - if (_initialized) return true; - - _ctx = SpiBusContext::get(); - if (!_ctx) return false; - - if (!_ctx->init(_timing)) { - SpiBusContext::release(); - _ctx = nullptr; - return false; - } - - _channelIdx = _ctx->registerChannel(_pin, this); - if (_channelIdx < 0) { - Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); - SpiBusContext::release(); - _ctx = nullptr; - return false; - } - - _initialized = true; - return true; -} - -void ParallelSpiBus::end() { - if (!_initialized) return; - - if (_ctx) { - uint32_t startWait = millis(); - while (!_ctx->isIdle()) { - if (millis() - startWait > 500) { - break; - } - vTaskDelay(1); - } - _ctx->unregisterChannel(_channelIdx); - SpiBusContext::release(); - _ctx = nullptr; - } - - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } - - _initialized = false; -} - -bool ParallelSpiBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) return true; - - if (_encodeBuffer) heap_caps_free(_encodeBuffer); - - _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; - return true; -} - -bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - - // Wait for previous transmission - uint32_t startWait = millis(); - while (!_ctx->isIdle()) { - if (millis() - startWait > 500) { - _ctx->forceIdle(); - return false; - } - vTaskDelay(1); - } - - // Wait for SPI to finish any remaining transfer - startWait = millis(); - while (!_ctx->isSpiDone()) { - if (millis() - startWait > 500) { - _ctx->forceIdle(); - return false; - } - vTaskDelay(1); - } - - if (!allocateBuffer(_numPixels)) return false; - - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); - - for (uint16_t i = 0; i < _numPixels; i++) { - encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); - dst += numCh; - } - - _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); - - return _ctx->startTransmit(); -} - -bool ParallelSpiBus::canShow() const { - if (!_ctx) return true; - return _ctx->isIdle() && _ctx->isSpiDone(); -} - -void ParallelSpiBus::waitComplete() { - while (_ctx && !_ctx->isIdle()) { - vTaskDelay(1); - } -} - -void ParallelSpiBus::setColorOrder(ColorOrder order) { - _order = order; -} - -void ParallelSpiBus::setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp) { - IBus::setPixelColor(pix, c, cp); -} - -#endif // WLEDPB_SPI_SUPPORT //============================================================================== // Bus Factory Implementation diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index b349ab3c99..a046e5a805 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -294,471 +294,7 @@ class ColorEncoder { uint8_t _numChannels; }; -//============================================================================== -// RMT Bus - Works on all ESP32 variants -//============================================================================== - -#include "driver/rmt.h" - -class RmtBus : public IBus { -public: - /** - * Create RMT bus - * @param pin GPIO pin - * @param timing LED timing - * @param order Color order - * @param channel RMT channel (-1 for auto) - */ - RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, - int8_t channel = -1); - ~RmtBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "RMT"; } - - // Configuration - void setInverted(bool inv) { _inverted = inv; } - void setTiming(const LedTiming& timing); - void setColorOrder(ColorOrder order); - - // Reset the auto-allocation counter (call before re-creating buses) - static void resetAutoChannel() { s_nextAutoChannel = 0; } - -private: - int8_t _pin; - int8_t _channel; - LedTiming _timing; - ColorOrder _order; - bool _inverted; - bool _initialized; - - rmt_channel_t _rmtChannel; - uint32_t _rmtBit0; - uint32_t _rmtBit1; - uint16_t _rmtResetTicks; - - // Encode buffer - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; - - static uint8_t s_nextAutoChannel; // auto-allocation counter - static uint8_t s_activeChannelMask; // bitmask of initialized channels - - void updateRmtTiming(); - bool allocateBuffer(uint16_t numPixels); - - // Static translate callback - static void IRAM_ATTR translateCB(const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num); -}; - -//============================================================================== -// I2S Parallel Bus - ESP32 and ESP32-S2 -//============================================================================== - -#ifdef WLEDPB_I2S_SUPPORT - -#include "soc/i2s_struct.h" -#include "soc/i2s_reg.h" -#include "driver/periph_ctrl.h" -#include "rom/lldesc.h" -#include "esp_intr_alloc.h" - -#if defined(WLEDPB_ESP32) - #define WLEDPB_I2S_BUS_COUNT 2 - #define WLEDPB_I2S_MAX_CHANNELS 16 -#else - #define WLEDPB_I2S_BUS_COUNT 1 - #define WLEDPB_I2S_MAX_CHANNELS 8 -#endif - -/** - * I2S bus context - manages shared I2S peripheral for parallel output - * Uses double-buffered DMA with ISR-driven buffer refill - */ -class I2sBusContext { -public: - static I2sBusContext* get(uint8_t busNum); - static void release(uint8_t busNum); - - bool init(const LedTiming& timing, size_t bufferSize); - void deinit(); - - // Channel management - int8_t registerChannel(int8_t pin, I2sBus* bus); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } - - // Transmission - bool startTransmit(); - bool isIdle() const { return _state == DriverState::Idle; } - - // Data access for channels - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); - -private: - I2sBusContext(uint8_t busNum); - ~I2sBusContext(); - - void fillBuffer(uint8_t bufIdx); - static void IRAM_ATTR dmaISR(void* arg); - - uint8_t _busNum; - i2s_dev_t* _i2sDev; - volatile DriverState _state; - bool _initialized; - - // DMA double buffer - lldesc_t* _dmaDesc[2]; - uint8_t* _dmaBuffer[2]; - size_t _bufferSize; - volatile uint8_t _activeBuffer; - - // Timing - LedTiming _timing; - uint32_t _clockDiv; - - // ISR handle - intr_handle_t _isrHandle; - - // Channel data - struct ChannelData { - I2sBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - size_t srcPos; - bool active; - }; - ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; - uint8_t _channelCount; - uint16_t _channelMask; - uint16_t _stagedMask; - size_t _maxDataLen; - - // Encoding (4-step cadence) - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); - - // Singleton instances - static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; - static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; -}; - -/** - * I2S parallel output bus - */ -class I2sBus : public IBus { -public: - /** - * Create I2S bus - * @param pin GPIO pin - * @param timing LED timing - * @param order Color order - * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) - * @param bufferSize DMA buffer size - */ - I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, - uint8_t busNum = 1, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); - ~I2sBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "I2S"; } - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order); - -private: - int8_t _pin; - uint8_t _busNum; - size_t _bufferSize; - LedTiming _timing; - ColorOrder _order; - bool _initialized; - - int8_t _channelIdx; - I2sBusContext* _ctx; - - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; - - bool allocateBuffer(uint16_t numPixels); -}; - -#endif // WLEDPB_I2S_SUPPORT - -//============================================================================== -// LCD Parallel Bus - ESP32-S3 only -//============================================================================== -#ifdef WLEDPB_LCD_SUPPORT - -#include "driver/periph_ctrl.h" -#include "esp_private/gdma.h" -#include "esp_rom_gpio.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" - -#ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE 1024 //2048 -> 2048 works well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) -#endif - -#ifndef WLEDPB_LCD_CADENCE_STEPS -#define WLEDPB_LCD_CADENCE_STEPS 4 -#endif - -#if WLEDPB_LCD_DEBUG - #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) -#else - #define LCD_LOG(fmt, ...) -#endif - -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); -static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); - -#define WLEDPB_LCD_MAX_CHANNELS 8 - -class LcdBusContext { -public: - static LcdBusContext* get(); - static void release(); - - bool init(const LedTiming& timing, size_t bufferSize, bool use16Bit = false); - void deinit(); - - int8_t registerChannel(int8_t pin, LcdBus* bus); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } - - bool startTransmit(); - bool isIdle() const { return _state == DriverState::Idle; } - - void forceIdle() { - LCD_CAM.lcd_user.lcd_start = 0; - if (_dmaChannel) gdma_stop(_dmaChannel); - _state = DriverState::Idle; - } - - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); - void printDebugStats(); - -private: - LcdBusContext(); - ~LcdBusContext(); - - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); - void fillBuffer(uint8_t bufIdx); - - static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, - gdma_event_data_t* event_data, - void* user_data); - - volatile DriverState _state; - bool _initialized; - bool _use16Bit; - - // DMA (circular linked list - never modified during operation) - gdma_channel_handle_t _dmaChannel; - dma_descriptor_t* _dmaDesc[2]; - uint8_t* _dmaBuffer[2]; - volatile uint8_t _activeBuffer; - - // Timing - LedTiming _timing; - size_t _bufferSize; - - // Channels - struct ChannelData { - LcdBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - size_t srcPos; - bool active; - }; - ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; - uint8_t _channelCount; - uint8_t _channelMask; - uint8_t _stagedMask; - size_t _maxDataLen; - - static LcdBusContext* _instance; - static uint8_t _refCount; - - friend class LcdBus; -}; - -class LcdBus : public IBus { -public: - LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, - size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, bool use16Bit = false); - ~LcdBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "LCD"; } - - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order); - -private: - bool allocateBuffer(uint16_t numPixels); - - int8_t _pin; - size_t _bufferSize; - bool _use16Bit; - LedTiming _timing; - ColorOrder _order; - bool _initialized; - - int8_t _channelIdx; - LcdBusContext* _ctx; - - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; - size_t _encodedLen; -}; - -#endif // WLEDPB_LCD_SUPPORT - -//============================================================================== -// SPI Parallel Bus - ESP32-C3 (uses SPI2 quad mode + GDMA) -//============================================================================== -#ifdef WLEDPB_SPI_SUPPORT - -#define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines -#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 -#define WLEDPB_SPI_GDMA_CHANNEL 0 - -class ParallelSpiBus; - -/** - * SPI bus context - manages SPI2 quad mode for parallel LED output on C3 - * Uses GDMA with circular linked-list and ISR-driven buffer refill - */ -class SpiBusContext { -public: - static SpiBusContext* get(); - static void release(); - - bool init(const LedTiming& timing); - void deinit(); - - int8_t registerChannel(int8_t pin, ParallelSpiBus* bus); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } - - bool startTransmit(); - bool isIdle() const { return !_sending; } - bool isSpiDone(); - void forceIdle(); - - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); - -private: - SpiBusContext(); - ~SpiBusContext(); - - void encodeSpiChunk(uint8_t bufIdx); - void resetAndStart(); - - static void IRAM_ATTR gdmaISR(void* arg); - static void IRAM_ATTR spiISR(void* arg); - - volatile bool _sending; - bool _initialized; - bool _hasStarted; - volatile uint8_t _currentBuffer; - - // DMA - uint8_t* _dmaBuffer[2]; - lldesc_t _dmaDesc[2]; - intr_handle_t _isrHandle; - intr_handle_t _spiIsrHandle; - - // SPI device - spi_dev_t* _hw; - - // Source data per channel - struct ChannelData { - ParallelSpiBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - bool active; - }; - ChannelData _channels[WLEDPB_SPI_MAX_CHANNELS]; - uint8_t _channelCount; - size_t _framePos; // current source byte position - size_t _numBytes; // total source bytes to send - mutable uint32_t _lastTransmitMs; - - uint8_t _stagedMask; - uint8_t _channelMask; - - static SpiBusContext* _instance; - static uint8_t _refCount; -}; - -/** - * SPI parallel output bus (for ESP32-C3) - */ -class ParallelSpiBus : public IBus { -public: - ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); - ~ParallelSpiBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "SPI"; } - - void setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) override; - - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order); - -private: - bool allocateBuffer(uint16_t numPixels); - - int8_t _pin; - LedTiming _timing; - ColorOrder _order; - bool _initialized; - - int8_t _channelIdx; - SpiBusContext* _ctx; - - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; -}; - -#endif // WLEDPB_SPI_SUPPORT //============================================================================== // Bus Factory - Create appropriate bus for platform @@ -830,4 +366,9 @@ static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t ch return mem; } -} // namespace WLEDpixelBus \ No newline at end of file +} // namespace WLEDpixelBus + +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_LCD.h" +#include "WLEDpixelBus_ParallelSpi.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp new file mode 100644 index 0000000000..261ea9ce0e --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -0,0 +1,666 @@ +#include "WLEDpixelBus.h" +#include "WLEDpixelBus_I2S.h" + +namespace WLEDpixelBus { + +//============================================================================== +// I2S Bus Implementation +//============================================================================== + +#ifdef WLEDPB_I2S_SUPPORT + +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); + } + _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; + } +} + +I2sBusContext::I2sBusContext(uint8_t busNum) + : _busNum(busNum) + , _state(DriverState::Idle) + , _initialized(false) + , _bufferSize(0) + , _activeBuffer(0) + , _timing{0, 0, 0, 0, 0} + , _clockDiv(1) + , _isrHandle(nullptr) + , _channelCount(0) + , _channelMask(0) + , _stagedMask(0) + , _maxDataLen(0) +{ +#if defined(WLEDPB_ESP32) + _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; +#else + _i2sDev = &I2S0; +#endif + + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + + _dmaDesc[0] = _dmaDesc[1] = nullptr; + _dmaBuffer[0] = _dmaBuffer[1] = nullptr; +} + +I2sBusContext:: ~I2sBusContext() { + deinit(); +} + +bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { + if (_initialized) return true; + + _timing = timing; + _bufferSize = (bufferSize + 3) & ~3; // align to 4 bytes + + // Allocate DMA buffers (4-byte aligned for DMA) + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + log_e("I2S DMA buffer alloc failed"); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, _bufferSize); + + _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + log_e("I2S DMA desc alloc failed"); + deinit(); + return false; + } + } + + // Setup DMA descriptors - circular chain for gapless ping-pong + for (int i = 0; i < 2; i++) { + _dmaDesc[i]->size = _bufferSize; + _dmaDesc[i]->length = _bufferSize; + _dmaDesc[i]->buf = _dmaBuffer[i]; + _dmaDesc[i]->eof = 1; // Generate interrupt on completion + _dmaDesc[i]->sosf = 0; + _dmaDesc[i]->owner = 1; + } + _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; + _dmaDesc[1]->qe.stqe_next = _dmaDesc[0]; + + // Enable I2S peripheral +#if defined(WLEDPB_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 8-bit parallel LCD mode (matching NeoPixelBus) + _i2sDev->conf2.val = 0; + _i2sDev->conf2.lcd_en = 1; + _i2sDev->conf2.lcd_tx_wrx2_en = 1; // Required for 8-bit parallel output + _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(WLEDPB_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 = 1; // 16-bit single channel + _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold + + // PCM bypass + _i2sDev->conf1.val = 0; + _i2sDev->conf1.tx_stop_en = 0; + _i2sDev->conf1.tx_pcm_bypass = 1; + + // Channel config + _i2sDev->conf_chan.val = 0; + _i2sDev->conf_chan.tx_chan_mod = 1; // Right channel + + // I2S conf + _i2sDev->conf.val = 0; + _i2sDev->conf.tx_msb_shift = 0; // No shift in parallel mode + _i2sDev->conf.tx_right_first = 1; + + // Clear timing register + _i2sDev->timing.val = 0; + + // Calculate clock divider for 4-step cadence (matching NeoPixelBus) + // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) + // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns + // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) + const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 + uint32_t bitPeriodNs = timing.bitPeriod(); + +#if defined(WLEDPB_ESP32) + const double baseClockMhz = 160.0; +#else + const double baseClockMhz = 80.0; +#endif + + // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz + // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 + double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; + if (clkmdiv < 2.0) clkmdiv = 2.0; + if (clkmdiv > 255.0) clkmdiv = 255.0; + + uint8_t clkmInteger = (uint8_t)clkmdiv; + double clkmFraction = clkmdiv - clkmInteger; + + // Convert fraction to divB/divA (fraction = divB/divA) + uint8_t divB = 0; + uint8_t divA = 0; + if (clkmFraction > 0.001) { + divA = 63; // use max denominator for best precision + divB = (uint8_t)(clkmFraction * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + _clockDiv = clkmInteger; + + Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", + bitPeriodNs, clkmInteger, divB, divA, bckDiv); + double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; + if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; + Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", + actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); + + // Set clock (with fractional divider for accurate timing) + _i2sDev->clkm_conf.val = 0; + _i2sDev->clkm_conf.clk_en = 1; + _i2sDev->clkm_conf.clkm_div_a = divA; + _i2sDev->clkm_conf.clkm_div_b = divB; + _i2sDev->clkm_conf.clkm_div_num = clkmInteger; + + // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) + _i2sDev->sample_rate_conf.val = 0; + _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; + _i2sDev->sample_rate_conf.tx_bits_mod = 8; + + // 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(WLEDPB_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_LEVEL1, + dmaISR, this, &_isrHandle); + if (err != ESP_OK) { + log_e("I2S ISR alloc failed: %d", err); + deinit(); + return false; + } + + _initialized = true; + Serial.printf("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); + return true; +} + +void I2sBusContext::deinit() { + if (_i2sDev) { + _i2sDev->int_ena.val = 0; // Disable interrupts first + _i2sDev->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } + _state = DriverState::Idle; + + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; + } + } + +#if defined(WLEDPB_ESP32) + periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); +#else + periph_module_disable(PERIPH_I2S0_MODULE); +#endif + + _initialized = false; +} + +int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { + // Find free slot + int8_t idx = -1; + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + if (! _channels[i].active) { + idx = i; + break; + } + } + + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + + // Configure GPIO + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + + // Route I2S output to GPIO + int sigIdx; +#if defined(WLEDPB_ESP32) + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; +#else + sigIdx = I2S0O_DATA_OUT0_IDX; +#endif + sigIdx += idx; + + gpio_matrix_out(pin, sigIdx, false, false); + + return idx; +} + +void I2sBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + if (len > _maxDataLen) { + _maxDataLen = len; + } + + // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() + if (_stagedMask & (1 << channelIdx)) { + _stagedMask = 0; + } + _stagedMask |= (1 << channelIdx); +} + +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { + // 4-step cadence encoding for parallel output + // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) + // Desired output: [HIGH][data][data][LOW] for each bit + // + // ESP32 I2S LCD mode with lcd_tx_wrx2_en=1 swaps half-words within 32-bit values: + // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] + // (NeoPixelBus documents this as "bytes within the words are swapped") + // So we write: p[0]=step2, p[1]=step3, p[2]=step0, p[3]=step1 + // + // Buffer is always filled completely (zeros = LOW = reset signal) + + memset(dest, 0, destLen); + size_t pos = 0; + + // Process each source byte position across all channels + while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte + bool hasData = false; + + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + hasData = true; + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint8_t chMask = (1 << ch); + + uint8_t* p = dest + pos; + for (int bit = 7; bit >= 0; bit--) { + uint8_t dataVal = (srcByte >> bit) & 1; + + // Half-word swapped: memory layout [step2, step3, step0, step1] + // Step 0 (HIGH) -> p[2] + p[2] |= chMask; + + // Step 1 (data) -> p[3] + if (dataVal) p[3] |= chMask; + + // Step 2 (data) -> p[0] + if (dataVal) p[0] |= chMask; + + // Step 3 (LOW) -> p[1] (already 0) + + p += 4; + } + } + + if (!hasData) break; + + // Advance all channel positions + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } + } + + pos += 32; + } + // Rest of buffer remains zero (reset signal) from memset +} + +void I2sBusContext::fillBuffer(uint8_t bufIdx) { + encode4Step(_dmaBuffer[bufIdx], _bufferSize); + // desc->length stays at _bufferSize (set in init, never changes) +} + +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 false; + _stagedMask = 0; // Reset for next frame + + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + // Fill both buffers initially + fillBuffer(0); + fillBuffer(1); + + // Restore DMA descriptor ownership (DMA clears owner to 0 after processing) + _dmaDesc[0]->owner = 1; + _dmaDesc[1]->owner = 1; + + _activeBuffer = 0; + _state = DriverState::Sending; + + // Reset DMA and FIFO before starting + _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; + + // Clear and enable interrupts + _i2sDev->int_clr.val = 0xFFFFFFFF; + _i2sDev->int_ena.out_eof = 1; + + // Enable DMA and start + _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; + + return true; +} + +static volatile uint32_t s_i2sIsrCount = 0; +static volatile uint32_t s_i2sIsrSending = 0; +static volatile uint32_t s_i2sIsrReset = 0; +static volatile uint32_t s_i2sIsrIdle = 0; + +void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { + I2sBusContext* ctx = (I2sBusContext*)arg; + i2s_dev_t* dev = ctx->_i2sDev; + + uint32_t status = dev->int_st.val; + dev->int_clr.val = status; + + if (!(status & I2S_OUT_EOF_INT_ST)) return; + + s_i2sIsrCount++; + + // The completed buffer just finished playing; DMA is now on the other buffer + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; + + if (ctx->_state == DriverState::Sending) { + s_i2sIsrSending++; + // Encode next chunk into the completed buffer + // encode4Step always fills the full buffer (zeros for any remainder) + ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); + + // Check if all source data has been consumed + bool moreData = false; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (ctx->_channels[ch].active && + ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { + moreData = true; + break; + } + } + if (!moreData) { + // Last data chunk was just encoded into completedBuf. + // DMA is currently playing the OTHER buffer. We need to wait + // for that to finish so the last-data buffer gets played. + ctx->_state = DriverState::SendingLast; + } + + // Restore DMA ownership so hardware can replay this buffer + ctx->_dmaDesc[completedBuf]->owner = 1; + } else if (ctx->_state == DriverState::SendingLast) { + // The other buffer just finished. DMA is now playing the last-data buffer. + // Fill completed buffer with zeros (reset signal) so it plays after. + memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); + ctx->_dmaDesc[completedBuf]->owner = 1; + s_i2sIsrReset++; + ctx->_state = DriverState::WaitingReset; + } else { + // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. + s_i2sIsrIdle++; + dev->int_ena.out_eof = 0; + dev->conf.tx_start = 0; + dev->out_link.start = 0; + ctx->_state = DriverState::Idle; + } +} + +// I2sBus implementation + +I2sBus::I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, + uint8_t busNum, size_t bufferSize) + : _pin(pin) + , _busNum(busNum) + , _bufferSize(bufferSize) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) +{ +} + +I2sBus::~I2sBus() { + end(); +} + +bool I2sBus::begin() { + if (_initialized) return true; + + _ctx = I2sBusContext::get(_busNum); + if (!_ctx) return false; + + if (!_ctx->init(_timing, _bufferSize)) { + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + Serial.printf("[I2S] registerChannel failed for pin %d\n", _pin); + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _initialized = true; + Serial.printf("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n", _pin, _busNum, _channelIdx); + return true; +} + +void I2sBus::end() { + if (!_initialized) return; + + if (_ctx) { + // Wait for any active transmission to complete before cleanup + while (!_ctx->isIdle()) vTaskDelay(1); + _ctx->unregisterChannel(_channelIdx); + I2sBusContext::release(_busNum); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool I2sBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + } + + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + // Always use internal pixel buffer (WLED always calls show() without args) + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + + // Wait for previous transmission to complete + while (!_ctx->isIdle()) { + vTaskDelay(1); + } + + if (!allocateBuffer(_numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } + + // Set data for our channel and start + _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); + return _ctx->startTransmit(); +} + +bool I2sBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void I2sBus::waitComplete() { + while (_ctx && !_ctx->isIdle()) { + vTaskDelay(1); + } +} + +void I2sBus::setColorOrder(ColorOrder order) { + _order = order; +} + +#endif // WLEDPB_I2S_SUPPORT + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h new file mode 100644 index 0000000000..a3f47d46b0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -0,0 +1,147 @@ +#pragma once + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +//============================================================================== +// I2S Parallel Bus - ESP32 and ESP32-S2 +//============================================================================== + +#ifdef WLEDPB_I2S_SUPPORT + +#include "soc/i2s_struct.h" +#include "soc/i2s_reg.h" +#include "driver/periph_ctrl.h" +#include "rom/lldesc.h" +#include "esp_intr_alloc.h" + +#if defined(WLEDPB_ESP32) + #define WLEDPB_I2S_BUS_COUNT 2 + #define WLEDPB_I2S_MAX_CHANNELS 16 +#else + #define WLEDPB_I2S_BUS_COUNT 1 + #define WLEDPB_I2S_MAX_CHANNELS 8 +#endif + +/** + * I2S bus context - manages shared I2S peripheral for parallel output + * Uses double-buffered DMA with ISR-driven buffer refill + */ +class I2sBusContext { +public: + static I2sBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); + + bool init(const LedTiming& timing, size_t bufferSize); + void deinit(); + + // Channel management + int8_t registerChannel(int8_t pin, I2sBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + // Transmission + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } + + // Data access for channels + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + I2sBusContext(uint8_t busNum); + ~I2sBusContext(); + + void fillBuffer(uint8_t bufIdx); + static void IRAM_ATTR dmaISR(void* arg); + + uint8_t _busNum; + i2s_dev_t* _i2sDev; + volatile DriverState _state; + bool _initialized; + + // DMA double buffer + lldesc_t* _dmaDesc[2]; + uint8_t* _dmaBuffer[2]; + size_t _bufferSize; + volatile uint8_t _activeBuffer; + + // Timing + LedTiming _timing; + uint32_t _clockDiv; + + // ISR handle + intr_handle_t _isrHandle; + + // Channel data + struct ChannelData { + I2sBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; + uint8_t _channelCount; + uint16_t _channelMask; + uint16_t _stagedMask; + size_t _maxDataLen; + + // Encoding (4-step cadence) + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + + // Singleton instances + static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; +}; + +/** + * I2S parallel output bus + */ +class I2sBus : public IBus { +public: + /** + * Create I2S bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) + * @param bufferSize DMA buffer size + */ + I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, + uint8_t busNum = 1, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + ~I2sBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "I2S"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); + +private: + int8_t _pin; + uint8_t _busNum; + size_t _bufferSize; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + int8_t _channelIdx; + I2sBusContext* _ctx; + + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + + bool allocateBuffer(uint16_t numPixels); +}; + +#endif // WLEDPB_I2S_SUPPORT + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp new file mode 100644 index 0000000000..f42308b3f1 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -0,0 +1,583 @@ +#include "WLEDpixelBus.h" +#include "WLEDpixelBus_LCD.h" + +namespace WLEDpixelBus { + +//============================================================================== +// LCD Bus Implementation (ESP32-S3) +//============================================================================== +/*------------------------------------------------------------------------- +WLEDpixelBus - LCD Implementation with Continuous DMA (Glitch-Free) + +Key design: +- DMA runs continuously in circular mode (buf0 -> buf1 -> buf0 -> ...) +- Buffers are filled with data or zeros (for reset period) +- Stop only after reset period completes on buffer boundary +- No DMA reconfiguration during transmission +-------------------------------------------------------------------------*/ + +#ifdef WLEDPB_LCD_SUPPORT + +#include "driver/periph_ctrl.h" +#include "esp_private/gdma.h" +#include "esp_rom_gpio.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" + +// ============================================ +// Configuration +// ============================================ + + +LcdBusContext* LcdBusContext::_instance = nullptr; +uint8_t LcdBusContext::_refCount = 0; + +LcdBusContext* LcdBusContext::get() { + if (_instance == nullptr) { + _instance = new LcdBusContext(); + } + _refCount++; + return _instance; +} + +void LcdBusContext::release() { + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } +} + +LcdBusContext::LcdBusContext() + : _state(DriverState::Idle) + , _initialized(false) + , _use16Bit(false) + , _dmaChannel(nullptr) + , _timing{0, 0, 0, 0, 0} + , _bufferSize(WLEDPB_LCD_DMA_BUFFER_SIZE) + , _channelCount(0) + , _channelMask(0) + , _stagedMask(0) + , _maxDataLen(0) + , _activeBuffer(0) +{ + for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + for (int i = 0; i < 2; i++) { + _dmaDesc[i] = nullptr; + _dmaBuffer[i] = nullptr; + } +} + +LcdBusContext::~LcdBusContext() { + deinit(); +} + +bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16Bit) { + if (_initialized) return true; + + LCD_LOG("Init: buf=%u x2, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", + WLEDPB_LCD_DMA_BUFFER_SIZE, WLEDPB_LCD_CADENCE_STEPS, + timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); + + _timing = timing; + _use16Bit = use16Bit; + _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; + + uint32_t bitPeriodNs = timing.bitPeriod(); + + // Allocate double DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + LCD_LOG("ERROR: DMA buffer %d alloc failed", i); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); + + _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + LCD_LOG("ERROR: DMA desc %d alloc failed", i); + deinit(); + return false; + } + memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); + } + + // Setup DMA descriptors in CIRCULAR mode (never changes during operation!) + // buf0 -> buf1 -> buf0 -> buf1 -> ... + for (int i = 0; i < 2; i++) { + _dmaDesc[i]->dw0.size = WLEDPB_LCD_DMA_BUFFER_SIZE; + _dmaDesc[i]->dw0.length = WLEDPB_LCD_DMA_BUFFER_SIZE; + _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR + _dmaDesc[i]->buffer = _dmaBuffer[i]; + _dmaDesc[i]->next = _dmaDesc[i ^ 1]; // Point to other buffer (circular) + } + + + + // 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 + double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; + + LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); + + if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { + LCD_LOG("ERROR: Invalid clock divider"); + deinit(); + 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; + LCD_CAM.lcd_user.lcd_2byte_en = use16Bit ? 1 : 0; + 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) { + LCD_LOG("ERROR: GDMA alloc failed: %d", err); + deinit(); + return false; + } + + err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + if (err != ESP_OK) { + LCD_LOG("ERROR: GDMA connect failed: %d", err); + deinit(); + return false; + } + + gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(_dmaChannel, &strategy_config); + + // Register DMA callback + gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; + gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); + + _initialized = true; + LCD_LOG("Init OK: clkm_div=%u+%u/%u", + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_num, + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_b, + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_a); + return true; +} + +void LcdBusContext::deinit() { + // Stop transmission + LCD_CAM.lcd_user.lcd_start = 0; + _state = DriverState::Idle; + + if (_dmaChannel) { + gdma_stop(_dmaChannel); + gdma_disconnect(_dmaChannel); + gdma_del_channel(_dmaChannel); + _dmaChannel = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; + } + } + + if (_initialized) { + periph_module_disable(PERIPH_LCD_CAM_MODULE); + } + + _initialized = false; +} + +int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus) { + int8_t idx = -1; + for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + + esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, false, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); + + LCD_LOG("Channel %d: pin=%d, mask=0x%02X", idx, pin, _channelMask); + return idx; +} + +void LcdBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_matrix_out(_channels[channelIdx].pin, SIG_GPIO_OUT_IDX, false, false); + pinMode(_channels[channelIdx].pin, INPUT); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; + + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; + + if (len > _maxDataLen) _maxDataLen = len; + + if (_stagedMask & (1 << channelIdx)) _stagedMask = 0; + _stagedMask |= (1 << channelIdx); +} + +void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { + // 4-step cadence encoding for parallel output without byte swapping + // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) + // Desired output: [HIGH][data][data][LOW] for each bit + // Buffer is always filled completely (zeros = LOW = reset signal) + + memset(dest, 0, destLen); + size_t pos = 0; + + // Process each source byte position across all channels + while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte + bool hasData = false; + + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + hasData = true; + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint8_t chMask = (1 << ch); + + uint8_t* p = dest + pos; + for (int bit = 7; bit >= 0; bit--) { + uint8_t dataVal = (srcByte >> bit) & 1; + + // Step 0 (HIGH) + p[0] |= chMask; + // Step 1 (data) + if (dataVal) p[1] |= chMask; + // Step 2 (data) + if (dataVal) p[2] |= chMask; + // Step 3 (LOW) - already 0 + p += 4; + } + } + + if (!hasData) break; + + // Advance all channel positions + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } + } + + pos += 32; + } + // Rest of buffer remains zero (reset signal) from memset +} + +void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { + encode4Step(_dmaBuffer[bufIdx], _bufferSize); +} + +bool LcdBusContext::startTransmit() { + if (_stagedMask != _channelMask) return false; // wait for all channels + _stagedMask = 0; + + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; + + // Reset channel positions + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) _maxDataLen = _channels[ch].srcLen; + } + } + if (_maxDataLen == 0) return false; + + // Fill both buffers initially + fillBuffer(0); + fillBuffer(1); + + _dmaDesc[0]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[1]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + + _activeBuffer = 0; + _state = DriverState::Sending; + + // Start DMA (circular mode - descriptors already linked) + 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; + + gdma_start(_dmaChannel, (intptr_t)_dmaDesc[0]); + esp_rom_delay_us(1); + LCD_CAM.lcd_user.lcd_start = 1; + + return true; +} + +IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data) { + LcdBusContext* ctx = (LcdBusContext*)user_data; + + // The completed buffer just finished playing; DMA is now on the other buffer + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; + + if (ctx->_state == DriverState::Sending) { + // Encode next chunk into the completed buffer + ctx->fillBuffer(completedBuf); + + // Check if all source data has been consumed + bool moreData = false; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (ctx->_channels[ch].active && + ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { + moreData = true; + break; + } + } + + if (!moreData) { + ctx->_state = DriverState::SendingLast; + } + + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + + } else if (ctx->_state == DriverState::SendingLast) { + // Fill completed buffer with zeros (reset signal) + memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + ctx->_state = DriverState::WaitingReset; + + } else { + // WaitingReset complete - stop DMA + LCD_CAM.lcd_user.lcd_start = 0; + gdma_stop(ctx->_dmaChannel); + ctx->_state = DriverState::Idle; + } + + return true; +} + +void LcdBusContext::printDebugStats() { + LCD_LOG("state=%u, channels=%u, mask=0x%02X", (unsigned)_state, _channelCount, _channelMask); +} + +// ============================================ +// LcdBus Implementation +// ============================================ + +LcdBus::LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, + size_t bufferSize, bool use16Bit) + : _pin(pin) + , _bufferSize(bufferSize) + , _use16Bit(use16Bit) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) + , _encodedLen(0) +{ +} + +LcdBus::~LcdBus() { + end(); +} + +bool LcdBus::begin() { + if (_initialized) return true; + + _ctx = LcdBusContext::get(); + if (!_ctx) return false; + + if (!_ctx->init(_timing, _bufferSize, _use16Bit)) { + LcdBusContext::release(); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + LcdBusContext::release(); + _ctx = nullptr; + return false; + } + + _initialized = true; + LCD_LOG("LcdBus: ch=%d pin=%d", _channelIdx, _pin); + return true; +} + +void LcdBus::end() { + if (!_initialized) return; + + if (_ctx) { + _ctx->unregisterChannel(_channelIdx); + LcdBusContext::release(); + _ctx = nullptr; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool LcdBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } + + if (_encodeBuffer) free(_encodeBuffer); + + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool LcdBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + // Always use internal pixel buffer (WLED always calls show() without args) + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + + // Wait for previous transmission to complete + uint32_t start = millis(); + while (!_ctx->isIdle()) { + if (millis() - start > 1000) { + _ctx->forceIdle(); + break; + } + delay(1); + } + + if (!allocateBuffer(_numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } + _encodedLen = _numPixels * numCh; + + // Set data for our channel and start transmission + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); + return _ctx->startTransmit(); +} + +bool LcdBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle(); +} + +void LcdBus::waitComplete() { + if (!_ctx) return; + uint32_t start = millis(); + while (!_ctx->isIdle()) { + if (millis() - start > 1000) { + _ctx->forceIdle(); + return; + } + delay(1); + } +} + +void LcdBus::setColorOrder(ColorOrder order) { + _order = order; +} + +#endif // WLEDPB_LCD_SUPPORT + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h new file mode 100644 index 0000000000..6abe6073e2 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -0,0 +1,150 @@ +#pragma once + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +//============================================================================== +// LCD Parallel Bus - ESP32-S3 only +//============================================================================== +#ifdef WLEDPB_LCD_SUPPORT + +#include "driver/periph_ctrl.h" +#include "esp_private/gdma.h" +#include "esp_rom_gpio.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" + +#ifndef WLEDPB_LCD_DMA_BUFFER_SIZE +#define WLEDPB_LCD_DMA_BUFFER_SIZE 1024 //2048 -> 2048 works well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) +#endif + +#ifndef WLEDPB_LCD_CADENCE_STEPS +#define WLEDPB_LCD_CADENCE_STEPS 4 +#endif + +#if WLEDPB_LCD_DEBUG + #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) +#else + #define LCD_LOG(fmt, ...) +#endif + +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); +static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); +static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); + +#define WLEDPB_LCD_MAX_CHANNELS 8 + +class LcdBusContext { +public: + static LcdBusContext* get(); + static void release(); + + bool init(const LedTiming& timing, size_t bufferSize, bool use16Bit = false); + void deinit(); + + int8_t registerChannel(int8_t pin, LcdBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } + + void forceIdle() { + LCD_CAM.lcd_user.lcd_start = 0; + if (_dmaChannel) gdma_stop(_dmaChannel); + _state = DriverState::Idle; + } + + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + void printDebugStats(); + +private: + LcdBusContext(); + ~LcdBusContext(); + + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + void fillBuffer(uint8_t bufIdx); + + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data); + + volatile DriverState _state; + bool _initialized; + bool _use16Bit; + + // DMA (circular linked list - never modified during operation) + gdma_channel_handle_t _dmaChannel; + dma_descriptor_t* _dmaDesc[2]; + uint8_t* _dmaBuffer[2]; + volatile uint8_t _activeBuffer; + + // Timing + LedTiming _timing; + size_t _bufferSize; + + // Channels + struct ChannelData { + LcdBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; + uint8_t _channelCount; + uint8_t _channelMask; + uint8_t _stagedMask; + size_t _maxDataLen; + + static LcdBusContext* _instance; + static uint8_t _refCount; + + friend class LcdBus; +}; + +class LcdBus : public IBus { +public: + LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, + size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, bool use16Bit = false); + ~LcdBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "LCD"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); + +private: + bool allocateBuffer(uint16_t numPixels); + + int8_t _pin; + size_t _bufferSize; + bool _use16Bit; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + int8_t _channelIdx; + LcdBusContext* _ctx; + + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + size_t _encodedLen; +}; + +#endif // WLEDPB_LCD_SUPPORT + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp new file mode 100644 index 0000000000..411151de0a --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -0,0 +1,629 @@ +#include "WLEDpixelBus.h" +#include "WLEDpixelBus_ParallelSpi.h" + +#ifdef WLEDPB_SPI_SUPPORT +#undef FLAG_ATTR +#define FLAG_ATTR(TYPE) +#include "hal/spi_ll.h" +#include "driver/periph_ctrl.h" +#include "esp_private/gdma.h" +#include "esp_rom_gpio.h" +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// SPI Parallel Bus Implementation (ESP32-C3) +//============================================================================== + +#ifdef WLEDPB_SPI_SUPPORT + +// low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) +static inline void spi_ll_apply_config(spi_dev_t *hw) +{ + hw->cmd.update = 1; + while (hw->cmd.update); //waiting config applied +} + +static inline void spi_ll_user_start(spi_dev_t *hw) +{ + hw->cmd.usr = 1; +} + + +// Pin assignments for SPI2 quad mode on C3 +// SPI2 signals: FSPID (MOSI/D0), FSPIQ (MISO/D1), FSPIWP (D2), FSPIHD (D3) +static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_OUT_IDX, FSPIHD_OUT_IDX }; + +// Encoding patterns for SPI quad mode (4-step cadence, LSB first) +// Each lane is one bit position in a nibble, one byte = two clock cycles, 2 bytes = one 4-step bit +static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high +static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high + +SpiBusContext* SpiBusContext::_instance = nullptr; +uint8_t SpiBusContext::_refCount = 0; + +SpiBusContext* SpiBusContext::get() { + if (_instance == nullptr) { + _instance = new SpiBusContext(); + } + _refCount++; + return _instance; +} + +void SpiBusContext::release() { + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } +} + +SpiBusContext::SpiBusContext() + : _sending(false) + , _initialized(false) + , _hasStarted(false) + , _currentBuffer(0) + , _isrHandle(nullptr) + , _spiIsrHandle(nullptr) + , _hw(&GPSPI2) + , _channelCount(0) + , _framePos(0) + , _numBytes(0) + , _lastTransmitMs(0) + , _stagedMask(0) + , _channelMask(0) +{ + _dmaBuffer[0] = _dmaBuffer[1] = nullptr; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, false}; + } +} + +SpiBusContext::~SpiBusContext() { + deinit(); +} + +bool SpiBusContext::isSpiDone() { + if (!_hasStarted) return true; // no transfer ever started + + // We are logically done once all real data is encoded + if (!_sending) { + return true; + } + + // Fail-safe watchdog: if stuck for > 250ms, recover it + if (millis() - _lastTransmitMs > 250) { + forceIdle(); + return true; + } + + return false; +} + +void SpiBusContext::forceIdle() { + _sending = false; + _stagedMask = 0; + + // Stop DMA immediately + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + if (_hw) { + _hw->cmd.usr = 0; + _hw->dma_int_clr.val = 0xFFFFFFFF; + } +} + +void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { + uint8_t* dst = _dmaBuffer[bufIdx]; + memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); + + if (!_sending) { + return; + } + + size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte + size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; + size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; + + if (srcThisChunk == 0) { + _sending = false; + _framePos = 0; + return; + } + + for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { + if (!_channels[lane].active || !_channels[lane].srcData) continue; + + size_t srcLen = _channels[lane].srcLen; + if (_framePos >= srcLen) continue; // Past the end of this lane's data, leave buffer 0 (low/no pulse) + + size_t validBytes = srcLen - _framePos; + if (validBytes > srcThisChunk) validBytes = srcThisChunk; + + const uint16_t zerobit = SPI_ZERO_BIT << lane; + const uint16_t onebit = SPI_ONE_BIT << lane; + const uint8_t* src = _channels[lane].srcData; + uint16_t* pOut = reinterpret_cast(dst); + + for (size_t i = 0; i < validBytes; i++) { + uint8_t v = src[_framePos + i]; + *pOut++ |= (v & 0x80) ? onebit : zerobit; + *pOut++ |= (v & 0x40) ? onebit : zerobit; + *pOut++ |= (v & 0x20) ? onebit : zerobit; + *pOut++ |= (v & 0x10) ? onebit : zerobit; + *pOut++ |= (v & 0x08) ? onebit : zerobit; + *pOut++ |= (v & 0x04) ? onebit : zerobit; + *pOut++ |= (v & 0x02) ? onebit : zerobit; + *pOut++ |= (v & 0x01) ? onebit : zerobit; + } + } + _framePos += srcThisChunk; +} + +// SPI ISR: handles trans_done (restart SPI bit counter) and outfifo_empty_err +// (FIFO underrun recovery). outfifo_empty_err is temporarily disabled after +// handling to prevent infinite ISR loops; trans_done re-enables it. +void IRAM_ATTR SpiBusContext::spiISR(void* arg) { + SpiBusContext* ctx = (SpiBusContext*)arg; + uint32_t raw = ctx->_hw->dma_int_raw.val; + + if (raw & 0x02) { + // outfifo_empty_err: FIFO starved, disable to prevent ISR loop + ctx->_hw->dma_int_ena.outfifo_empty_err = 0; + ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags + ctx->_hw->cmd.usr = 0; // Stop SPI + + spi_ll_dma_tx_fifo_reset(ctx->_hw); + spi_ll_outfifo_empty_clr(ctx->_hw); + + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + ctx->_dmaDesc[0].owner = 1; + ctx->_dmaDesc[1].owner = 1; + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&ctx->_dmaDesc[ctx->_currentBuffer]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + spi_ll_clear_int_stat(ctx->_hw); + spi_ll_set_mosi_bitlen(ctx->_hw, 253952); + ctx->_hw->cmd.usr = 1; + return; + } + + if (raw & 0x1000) { + ctx->_hw->dma_int_clr.trans_done = 1; + spi_ll_set_mosi_bitlen(ctx->_hw, 253952); + ctx->_hw->cmd.usr = 1; + ctx->_hw->dma_int_ena.outfifo_empty_err = 1; + } +} + +void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { + SpiBusContext* ctx = (SpiBusContext*)arg; + gdma_dev_t* dma = &GDMA; + + if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + uint8_t completedBuf = ctx->_currentBuffer; + ctx->encodeSpiChunk(completedBuf); + + // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI + ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + ctx->_dmaDesc[completedBuf].eof = 1; + ctx->_dmaDesc[completedBuf].owner = 1; + ctx->_currentBuffer = completedBuf ? 0 : 1; + } + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; +} + +bool SpiBusContext::init(const LedTiming& timing) { + if (_initialized) return true; + + // Allocate DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, + MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (!_dmaBuffer[i]) { + Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, WLEDPB_SPI_DMA_BUFFER_SIZE); + } + + // Setup DMA descriptors - circular linked list + for (int i = 0; i < 2; i++) { + _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].owner = 1; + _dmaDesc[i].sosf = 0; + _dmaDesc[i].eof = 1; + _dmaDesc[i].buf = _dmaBuffer[i]; + } + _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; + _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; + + // Enable peripheral clocks and force-reset (SPI2 + DMA) + // periph_module_enable() uses ref counting and may be a no-op if the + // peripheral was already enabled. Explicit reset ensures clean state. + periph_module_enable(PERIPH_SPI2_MODULE); + periph_module_reset(PERIPH_SPI2_MODULE); + periph_module_enable(PERIPH_GDMA_MODULE); + periph_module_reset(PERIPH_GDMA_MODULE); + + // Configure SPI2 master + spi_ll_master_init(_hw); + spi_ll_master_set_mode(_hw, 0); + spi_ll_set_tx_lsbfirst(_hw, true); + + _hw->user.usr_command = 0; + _hw->user.usr_addr = 0; + _hw->user.usr_dummy = 0; + _hw->user.usr_miso = 0; + _hw->user.usr_mosi = 1; + + // Clear idle output polarities for D2/D3 + _hw->ctrl.q_pol = 0; + _hw->ctrl.d_pol = 0; + _hw->ctrl.hold_pol = 0; + _hw->ctrl.wp_pol = 0; + + spi_line_mode_t linemode = {}; + linemode.data_lines = 4; // quad mode + spi_ll_master_set_line_mode(_hw, linemode); + + // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config + // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) + // Clock: 4 steps per bit → 4 SPI clock cycles per bit period. + // targetFreq = 4 / (bitPeriod_ns * 1e-9) = 4,000,000,000 / bitPeriod_ns + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t targetFreq = 4000000000UL / bitPeriodNs; + if (targetFreq < 2000000) targetFreq = 2000000; + if (targetFreq > 5000000) targetFreq = 5000000; + + spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); + + // Route SPI clock to a dummy pin (needed for DMA to work) + pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + + spi_ll_set_mosi_bitlen(_hw, 16384); + spi_ll_enable_mosi(_hw, true); + + spi_ll_dma_tx_enable(_hw, true); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + // Configure GDMA + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + + esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, + gdmaISR, this, &_isrHandle); + if (err != ESP_OK) { + Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + deinit(); + return false; + } + + // Install SPI ISR for trans_done and outfifo_empty_err recovery + _hw->dma_int_clr.val = 0xFFFFFFFF; + _hw->dma_int_ena.trans_done = 1; + _hw->dma_int_ena.outfifo_empty_err = 1; + err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, + spiISR, this, &_spiIsrHandle); + if (err != ESP_OK) { + Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); + deinit(); + return false; + } + + _initialized = true; + return true; +} + +void SpiBusContext::deinit() { + _sending = false; + + // Stop SPI and DMA before freeing resources + if (_hw) { + _hw->cmd.usr = 0; // Stop SPI transfer + } + + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + if (_hw) { + _hw->dma_int_ena.trans_done = 0; // Disable SPI trans_done interrupt + } + + if (_spiIsrHandle) { + esp_intr_free(_spiIsrHandle); + _spiIsrHandle = nullptr; + } + + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + } + + periph_module_disable(PERIPH_SPI2_MODULE); + periph_module_disable(PERIPH_GDMA_MODULE); + _initialized = false; +} + +int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus) { + int8_t idx = -1; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; + } + } + if (idx < 0) return -1; + + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); + + // Route SPI data signal to GPIO + pinMode(pin, OUTPUT); + pinMatrixOutAttach(pin, SPI_SIGNAL_INDICES[idx], false, false); + + return idx; +} + +void SpiBusContext::unregisterChannel(int8_t channelIdx) { + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; + + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } + + _channels[channelIdx] = {nullptr, -1, nullptr, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); +} + +void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + + // Mark this channel as staged + _stagedMask |= (1 << channelIdx); + + if (len > _numBytes) _numBytes = len; +} + +void SpiBusContext::resetAndStart() { + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + _dmaDesc[0].owner = 1; + _dmaDesc[1].owner = 1; + + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; +} + +bool SpiBusContext::startTransmit() { + if (_sending) return false; + if (_channelCount == 0) return false; + + if (_stagedMask != _channelMask) return false; + _stagedMask = 0; + + _lastTransmitMs = millis(); + size_t newBytes = 0; + for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcLen > newBytes) { + newBytes = _channels[ch].srcLen; + } + } + + if (!_hasStarted) { + _framePos = 0; + _numBytes = newBytes; + + // Max buffer-aligned bit count: continuous output for ~97ms before + // trans_done restarts. outfifo_empty_err handles FIFO underruns. + spi_ll_set_mosi_bitlen(_hw, 253952); + spi_ll_clear_int_stat(_hw); + + _sending = true; + _currentBuffer = 0; + encodeSpiChunk(0); + encodeSpiChunk(1); + + resetAndStart(); + _hasStarted = true; + spi_ll_user_start(_hw); + } else { + // Hardware already running - just update pointers, ISR picks up new data + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + _framePos = 0; + _numBytes = newBytes; + _sending = true; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + } + + return true; +} + +// SpiBus implementation + +ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) + : _pin(pin) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) +{ +} + +ParallelSpiBus::~ParallelSpiBus() { + end(); +} + +bool ParallelSpiBus::begin() { + if (_initialized) return true; + + _ctx = SpiBusContext::get(); + if (!_ctx) return false; + + if (!_ctx->init(_timing)) { + SpiBusContext::release(); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); + SpiBusContext::release(); + _ctx = nullptr; + return false; + } + + _initialized = true; + return true; +} + +void ParallelSpiBus::end() { + if (!_initialized) return; + + if (_ctx) { + uint32_t startWait = millis(); + while (!_ctx->isIdle()) { + if (millis() - startWait > 500) { + break; + } + vTaskDelay(1); + } + _ctx->unregisterChannel(_channelIdx); + SpiBusContext::release(); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool ParallelSpiBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) return true; + + if (_encodeBuffer) heap_caps_free(_encodeBuffer); + + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + + // Wait for previous transmission + uint32_t startWait = millis(); + while (!_ctx->isIdle()) { + if (millis() - startWait > 500) { + _ctx->forceIdle(); + return false; + } + vTaskDelay(1); + } + + // Wait for SPI to finish any remaining transfer + startWait = millis(); + while (!_ctx->isSpiDone()) { + if (millis() - startWait > 500) { + _ctx->forceIdle(); + return false; + } + vTaskDelay(1); + } + + if (!allocateBuffer(_numPixels)) return false; + + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } + + _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); + + return _ctx->startTransmit(); +} + +bool ParallelSpiBus::canShow() const { + if (!_ctx) return true; + return _ctx->isIdle() && _ctx->isSpiDone(); +} + +void ParallelSpiBus::waitComplete() { + while (_ctx && !_ctx->isIdle()) { + vTaskDelay(1); + } +} + +void ParallelSpiBus::setColorOrder(ColorOrder order) { + _order = order; +} + +void ParallelSpiBus::setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp) { + IBus::setPixelColor(pix, c, cp); +} + +#endif // WLEDPB_SPI_SUPPORT + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h new file mode 100644 index 0000000000..28d58210a0 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -0,0 +1,125 @@ +#pragma once + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +//============================================================================== +// SPI Parallel Bus - ESP32-C3 (uses SPI2 quad mode + GDMA) +//============================================================================== +#ifdef WLEDPB_SPI_SUPPORT + +#define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines +#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 +#define WLEDPB_SPI_GDMA_CHANNEL 0 + +class ParallelSpiBus; + +/** + * SPI bus context - manages SPI2 quad mode for parallel LED output on C3 + * Uses GDMA with circular linked-list and ISR-driven buffer refill + */ +class SpiBusContext { +public: + static SpiBusContext* get(); + static void release(); + + bool init(const LedTiming& timing); + void deinit(); + + int8_t registerChannel(int8_t pin, ParallelSpiBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } + + bool startTransmit(); + bool isIdle() const { return !_sending; } + bool isSpiDone(); + void forceIdle(); + + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + +private: + SpiBusContext(); + ~SpiBusContext(); + + void encodeSpiChunk(uint8_t bufIdx); + void resetAndStart(); + + static void IRAM_ATTR gdmaISR(void* arg); + static void IRAM_ATTR spiISR(void* arg); + + volatile bool _sending; + bool _initialized; + bool _hasStarted; + volatile uint8_t _currentBuffer; + + // DMA + uint8_t* _dmaBuffer[2]; + lldesc_t _dmaDesc[2]; + intr_handle_t _isrHandle; + intr_handle_t _spiIsrHandle; + + // SPI device + spi_dev_t* _hw; + + // Source data per channel + struct ChannelData { + ParallelSpiBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + bool active; + }; + ChannelData _channels[WLEDPB_SPI_MAX_CHANNELS]; + uint8_t _channelCount; + size_t _framePos; // current source byte position + size_t _numBytes; // total source bytes to send + mutable uint32_t _lastTransmitMs; + + uint8_t _stagedMask; + uint8_t _channelMask; + + static SpiBusContext* _instance; + static uint8_t _refCount; +}; + +/** + * SPI parallel output bus (for ESP32-C3) + */ +class ParallelSpiBus : public IBus { +public: + ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~ParallelSpiBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "SPI"; } + + void setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) override; + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); + +private: + bool allocateBuffer(uint16_t numPixels); + + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + int8_t _channelIdx; + SpiBusContext* _ctx; + + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; +}; + +#endif // WLEDPB_SPI_SUPPORT + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp new file mode 100644 index 0000000000..afc66d61b4 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -0,0 +1,297 @@ +#include "WLEDpixelBus.h" +#include "WLEDpixelBus_RMT.h" + +namespace WLEDpixelBus { + +//============================================================================== +// RMT Bus Implementation +//============================================================================== + +// Context for RMT translate callback - must be in DRAM for IRAM ISR access +static DRAM_ATTR struct { + uint32_t bit0; + uint32_t bit1; + uint16_t resetDuration; +} s_rmtCtx; + +// Static auto-channel counter for RmtBus +uint8_t RmtBus::s_nextAutoChannel = 0; +uint8_t RmtBus::s_activeChannelMask = 0; + +RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) + : _pin(pin) + , _channel(channel) + , _timing(timing) + , _order(order) + , _inverted(false) + , _initialized(false) + , _rmtChannel(RMT_CHANNEL_0) + , _rmtBit0(0) + , _rmtBit1(0) + , _rmtResetTicks(0) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) +{ +} + +RmtBus::~RmtBus() { + end(); +} + +void RmtBus::updateRmtTiming() { + // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick + const uint8_t clockDiv = 2; + 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; + }; + + uint16_t t0h = nsToTicks(_timing.t0h_ns); + uint16_t t0l = nsToTicks(_timing.t0l_ns); + uint16_t t1h = nsToTicks(_timing.t1h_ns); + uint16_t t1l = nsToTicks(_timing.t1l_ns); + + rmt_item32_t bit0, bit1; + + if (_inverted) { + bit0.level0 = 0; bit0.duration0 = t0h; + bit0.level1 = 1; bit0.duration1 = t0l; + bit1.level0 = 0; bit1.duration0 = t1h; + bit1.level1 = 1; bit1.duration1 = t1l; + } else { + bit0.level0 = 1; bit0.duration0 = t0h; + bit0.level1 = 0; bit0.duration1 = t0l; + bit1.level0 = 1; bit1.duration0 = t1h; + bit1.level1 = 0; bit1.duration1 = t1l; + } + + _rmtBit0 = bit0.val; + _rmtBit1 = bit1.val; + _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); +} + +bool RmtBus::begin() { + if (_initialized) return true; + + // Auto-select channel if needed + if (_channel < 0) { + // Use static counter for auto-allocation (fallback if caller didn't specify) + if (s_nextAutoChannel >= getRmtMaxChannels()) { + return false; + } + _channel = s_nextAutoChannel++; + } + + if (_channel >= (int8_t)getRmtMaxChannels()) { + Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, getRmtMaxChannels()); + return false; + } + _rmtChannel = (rmt_channel_t)_channel; + + updateRmtTiming(); + + rmt_config_t config = {}; + + config.rmt_mode = RMT_MODE_TX; + config.channel = _rmtChannel; + config.gpio_num = (gpio_num_t)_pin; + config.mem_block_num = 1; + config.clk_div = 2; // 40MHz + + config.tx_config.loop_en = false; + config.tx_config.carrier_en = false; + //config.tx_config.carrier_freq_hz = 38000; + //config.tx_config.carrier_duty_percent = 33; + config.tx_config.idle_output_en = true; + config.tx_config.idle_level = _inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW; + + + esp_err_t err = rmt_config(&config); + if (err != ESP_OK) { + return false; + } + + // Prioritize RMT over I2S/SPI DMA interrupts (which use LEVEL1) to prevent starvation. + // Try LEVEL3 first, fallback to LEVEL2, then LEVEL1. + int flags = ESP_INTR_FLAG_IRAM; +#ifdef ESP_INTR_FLAG_LEVEL3 + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL3); + if (err != ESP_OK) +#endif + { +#ifdef ESP_INTR_FLAG_LEVEL2 + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL2); + if (err != ESP_OK) +#endif + { + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL1); + } + } + if (err != ESP_OK) { + return false; + } + + err = rmt_translator_init(_rmtChannel, translateCB); + if (err != ESP_OK) { + rmt_driver_uninstall(_rmtChannel); + return false; + } + + _initialized = true; + s_activeChannelMask |= (1 << _channel); + return true; +} + +void RmtBus::end() { + if (!_initialized) return; + + s_activeChannelMask &= ~(1 << _channel); + rmt_driver_uninstall(_rmtChannel); + + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; +} + +bool RmtBus::allocateBuffer(uint16_t numPixels) { + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + } + + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; +} + +bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!pixels) { + pixels = _pixelData; + numPixels = _numPixels; + cct = _cctData; + } + if (numPixels == 0) numPixels = _numPixels; + if (!cct) cct = _cctData; + + if (!_initialized || !pixels || numPixels == 0) { + return false; + } + + // Wait for previous transmission on THIS channel to complete + rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); + + if (!allocateBuffer(numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + dst += numCh; + } + + // Update context for ISR + s_rmtCtx.bit0 = _rmtBit0; + s_rmtCtx.bit1 = _rmtBit1; + s_rmtCtx.resetDuration = _rmtResetTicks; + + // Start transmission + size_t dataLen = numPixels * numCh; + esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); + + return err == ESP_OK; +} + +bool RmtBus::canShow() const { + if (!_initialized) return true; + // Use rmt_wait_tx_done with 0 timeout to check if TX is done (matching NeoPixelBus) + return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); +} + +void RmtBus::waitComplete() { + if (_initialized) { + rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + } +} + +void RmtBus::setTiming(const LedTiming& timing) { + _timing = timing; + if (_initialized) { + updateRmtTiming(); + } +} + +void RmtBus::setColorOrder(ColorOrder order) { + _order = order; +} + +void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num) { + if (src == nullptr || dest == nullptr) { + *translated_size = 0; + *item_num = 0; + return; + } + + const uint8_t* psrc = (const uint8_t*)src; + rmt_item32_t* pdest = dest; + size_t size = 0; + size_t num = 0; + + uint32_t bit0 = s_rmtCtx.bit0; + uint32_t bit1 = s_rmtCtx.bit1; + uint16_t resetDuration = s_rmtCtx.resetDuration; + + // Each byte produces 8 RMT items + for (;;) + { + uint8_t data = *psrc; + + for (uint8_t bit = 0; bit < 8; bit++) + { + pdest->val = (data & 0x80) ? bit1 : bit0; + pdest++; + data <<= 1; + } + num += 8; + size++; + + // If this is the last byte, extend the last bit's LOW duration + // to include the full reset signal length + if (size >= src_size) + { + pdest--; + pdest->duration1 = resetDuration; + break; + } + + if (num >= wanted_num) + { + break; + } + + psrc++; + } + + *translated_size = size; + *item_num = num; +} + +} // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h new file mode 100644 index 0000000000..404ee618a8 --- /dev/null +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -0,0 +1,72 @@ +#pragma once + +#include "WLEDpixelBus.h" + +namespace WLEDpixelBus { + +//============================================================================== +// RMT Bus - Works on all ESP32 variants +//============================================================================== + +#include "driver/rmt.h" + +class RmtBus : public IBus { +public: + /** + * Create RMT bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @param channel RMT channel (-1 for auto) + */ + RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, + int8_t channel = -1); + ~RmtBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "RMT"; } + + // Configuration + void setInverted(bool inv) { _inverted = inv; } + void setTiming(const LedTiming& timing); + void setColorOrder(ColorOrder order); + + // Reset the auto-allocation counter (call before re-creating buses) + static void resetAutoChannel() { s_nextAutoChannel = 0; } + +private: + int8_t _pin; + int8_t _channel; + LedTiming _timing; + ColorOrder _order; + bool _inverted; + bool _initialized; + + rmt_channel_t _rmtChannel; + uint32_t _rmtBit0; + uint32_t _rmtBit1; + uint16_t _rmtResetTicks; + + // Encode buffer + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + + static uint8_t s_nextAutoChannel; // auto-allocation counter + static uint8_t s_activeChannelMask; // bitmask of initialized channels + + void updateRmtTiming(); + bool allocateBuffer(uint16_t numPixels); + + // Static translate callback + static void IRAM_ATTR translateCB(const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num); +}; + +} // namespace WLEDpixelBus From fecf36da1e91d511804a95bebe676f7e419bb77b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 11:47:43 +0100 Subject: [PATCH 029/173] fix RGB-CCT --- wled00/bus_wrapper.h | 12 +++++++++++- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 8 ++++++++ wled00/src/WLEDpixelBus/WLEDpixelBus.h | 9 ++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index cb6750a229..5d2157383d 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -152,7 +152,17 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t wledOrder = (uint8_t)colorOrder & 0x0F; WLEDpixelBus::ColorOrder finalOrder = WLEDpixelBus::ColorOrder::GRB; - if (proto.channels >= 4) { + if (proto.channels >= 5) { + switch (wledOrder) { + case 0: finalOrder = WLEDpixelBus::ColorOrder::GRBWC; break; + case 1: finalOrder = WLEDpixelBus::ColorOrder::RGBWC; break; + case 2: finalOrder = WLEDpixelBus::ColorOrder::BRGWC; break; + case 3: finalOrder = WLEDpixelBus::ColorOrder::RBGWC; break; + case 4: finalOrder = WLEDpixelBus::ColorOrder::BGRWC; break; + case 5: finalOrder = WLEDpixelBus::ColorOrder::GBRWC; break; + default: finalOrder = WLEDpixelBus::ColorOrder::GRBWC; break; + } + } else if (proto.channels == 4) { switch (wledOrder) { case 0: finalOrder = WLEDpixelBus::ColorOrder::GRBW; break; case 1: finalOrder = WLEDpixelBus::ColorOrder::RGBW; break; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index a9e08c0ebb..52de8532e3 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -59,6 +59,14 @@ void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) con case ColorOrder::WGBR: out[0]=w; out[1]=g; out[2]=b; out[3]=r; break; case ColorOrder::WBGR: out[0]=w; out[1]=b; out[2]=g; out[3]=r; break; + // RGBWC variants + case ColorOrder::RGBWC: out[0]=r; out[1]=g; out[2]=b; out[3]=ww; out[4]=cw; break; + case ColorOrder::GRBWC: out[0]=g; out[1]=r; out[2]=b; out[3]=ww; out[4]=cw; break; + case ColorOrder::BRGWC: out[0]=b; out[1]=r; out[2]=g; out[3]=ww; out[4]=cw; break; + case ColorOrder::RBGWC: out[0]=r; out[1]=b; out[2]=g; out[3]=ww; out[4]=cw; break; + case ColorOrder::GBRWC: out[0]=g; out[1]=b; out[2]=r; out[3]=ww; out[4]=cw; break; + case ColorOrder::BGRWC: out[0]=b; out[1]=g; out[2]=r; out[3]=ww; out[4]=cw; break; + default: out[0]=g; out[1]=r; out[2]=b; break; } } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index a046e5a805..89767c36a8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -103,13 +103,20 @@ enum class ColorOrder : uint8_t { WRBG = 19, WGBR = 20, WBGR = 21, + // RGBCWCW variants (5 bytes) + GRBWC = 30, + RGBWC = 31, + BRGWC = 32, + RBGWC = 33, + BGRWC = 34, + GBRWC = 35, }; /** * Get byte count per pixel for a color order */ inline constexpr uint8_t getChannelCount(ColorOrder order) { - return (static_cast(order) >= 10) ? 4 : 3; + return (static_cast(order) >= 30) ? 5 : ((static_cast(order) >= 10) ? 4 : 3); } /** From 6fe6ed88b2568df20d5c094a8a3dc53845b7aa3b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 13:40:28 +0100 Subject: [PATCH 030/173] change I2S timing calculation to match 0hi period --- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 6123dc0019..037d2006c3 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -4,6 +4,11 @@ namespace WLEDpixelBus { +// Note on timings regarding I2S/LCD/SPI buses (4 step cadence): +// the drivers will always use a 1/4 - 3/4 duty cycle for '0' and '1' bits (e.g. 250ns high + 750ns low for a 1us period) +// it only derives the total period from the given timings for RMT (0hi+0lo+1hi+1lo)/2) +// TODO: a better strategy might be to use 0hi*4 as the period as 0 timing is most critical + /** * LED timing parameters in nanoseconds */ @@ -17,18 +22,18 @@ struct LedTiming { 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 - constexpr uint32_t bitPeriod() const { - return (t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2; - } + // Calculate bit period in ns TODO: drop 3 step cadence support? + constexpr uint32_t bitPeriod(uint8_t cadenceSteps = 0) const { + return (cadenceSteps == 4) ? (t0h_ns * cadenceSteps) : ((t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2); + } }; // Predefined timing constants namespace Timing { // ---- Standard 1-wire LEDs (800KHz family) ---- - constexpr LedTiming WS2812 {300, 900, 800, 450, 100}; // WS2812B - constexpr LedTiming WS2811 {300, 950, 900, 350, 300}; // WS2811 (12V) - constexpr LedTiming WS2813 {400, 850, 800, 450, 300}; // WS2813 (backup data) + constexpr LedTiming WS2812 {300, 900, 700, 500, 100}; // WS2812B + constexpr LedTiming WS2811 {300, 900, 700, 500, 300}; // WS2811 (12V) + constexpr LedTiming WS2813 {300, 850, 700, 350, 300}; // WS2813 (backup data) constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; // WS2815 (12V, 255mA) constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; // WS2805 RGBCW (5ch) constexpr LedTiming SK6812 {300, 900, 800, 450, 200}; // SK6812 / SK6812 RGBW @@ -73,7 +78,7 @@ namespace Timing { * @return Clock divider value */ inline uint32_t calcClockDiv(const LedTiming& timing, uint8_t cadenceSteps, uint32_t baseClockMHz) { - uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); uint32_t stepPeriodNs = bitPeriodNs / cadenceSteps; uint32_t div = (baseClockMHz * stepPeriodNs) / 1000; return (div < 2) ? 2 : (div > 255) ? 255 : div; @@ -86,7 +91,7 @@ inline uint32_t calcClockDiv(const LedTiming& timing, uint8_t cadenceSteps, uint * @return Number of zero-bytes needed for reset period */ inline uint32_t calcResetBytes(const LedTiming& timing, uint8_t cadenceSteps) { - uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); uint32_t clockPeriodNs = bitPeriodNs / cadenceSteps; uint32_t bytesPerUs = (clockPeriodNs > 0) ? (1000 / clockPeriodNs) : 1; return timing.reset_us * bytesPerUs; From 5d5d7ff7598a0a453647445e48e70fa759ebf1e0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 13:40:55 +0100 Subject: [PATCH 031/173] add dynamic RMT channel assignment to maximize the memory blocks and reduce interrupt frequency --- wled00/FX_fcn.cpp | 18 +++++ wled00/bus_wrapper.h | 6 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 74 +++++++++++++++++--- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 12 +++- 5 files changed, 95 insertions(+), 17 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 8fc628e288..d0c87d98c7 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -11,6 +11,9 @@ */ #include "wled.h" #include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? +#if defined(ARDUINO_ARCH_ESP32) +#include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" +#endif /* Custom per-LED mapping has moved! @@ -1161,20 +1164,35 @@ void WS2812FX::finalizeInit() { BusManager::removeAll(); // TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers. unsigned digitalCount = 0; + unsigned rmtBusCount = 0; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) // validate the bus config: count I2S buses and check if they meet requirements unsigned i2sBusCount = 0; + #endif for (const auto &bus : busConfigs) { if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { digitalCount++; + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) if (bus.driverType == 1) i2sBusCount++; + else + rmtBusCount++; + #else + rmtBusCount++; + #endif } } + + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount); #endif + #if defined(ARDUINO_ARCH_ESP32) + WLEDpixelBus::RmtBus::resetAutoChannel(); + WLEDpixelBus::RmtBus::setExpectedChannels((uint8_t)rmtBusCount); + #endif + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); // create buses/outputs unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 5d2157383d..30650d5934 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -2,7 +2,6 @@ #ifndef BusWrapper_h #define BusWrapper_h -//#define NPB_CONF_4STEP_CADENCE #include "src/WLEDpixelBus/WLEDpixelBus.h" #include "src/WLEDpixelBus/WLEDpixelBus_SPI.h" #include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" @@ -220,11 +219,12 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #endif - int8_t rmtCh = -1; + int8_t rmtCh = -1; // -1 means auto select by RMT driver #ifndef ESP8266 if (btype == WLEDpixelBus::BusType::RMT) { if (_rmtChannel < WLED_MAX_RMT_CHANNELS) { - rmtCh = _rmtChannel++; + //rmtCh = _rmtChannel++; // assign channels in order, do not use auto-channel function (this uses 1 memory block per channel allowing RX RMT channels to be used as well) + _rmtChannel++; // increment channel count for tracking, but use auto-channel to optimize memory block allocation } else { return nullptr; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 261ea9ce0e..5473aa765c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -176,7 +176,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // Clear timing register _i2sDev->timing.val = 0; - // Calculate clock divider for 4-step cadence (matching NeoPixelBus) + // Calculate clock divider for 4-step cadence // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index afc66d61b4..186fc01aeb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -15,7 +15,10 @@ static DRAM_ATTR struct { } s_rmtCtx; // Static auto-channel counter for RmtBus -uint8_t RmtBus::s_nextAutoChannel = 0; +uint8_t RmtBus::s_expectedChannels = 1; +uint8_t RmtBus::s_allocatedCount = 0; +uint8_t RmtBus::s_currentChannelIndex = 0; +uint8_t RmtBus::s_usedBlocks = 0; uint8_t RmtBus::s_activeChannelMask = 0; RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) @@ -75,35 +78,71 @@ void RmtBus::updateRmtTiming() { bool RmtBus::begin() { if (_initialized) return true; - // Auto-select channel if needed + uint8_t blocksToUse = 1; + uint8_t maxTxChannels = getRmtMaxChannels(); + + // Auto-channel select with optimized memory block allocation (assumes no RMT RX usage) + // note on channel allocation: channels are assigned such as to maximize the number of memory blocks + // available to the channel to minimize the number of interrupts needed for buffer re-fills (less context switching overhead) + // C3 and S3 have less channels than blocks, so the last channel can always use additional blocks + // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks + // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 blocks, and CH3 with 5 blocks if (_channel < 0) { - // Use static counter for auto-allocation (fallback if caller didn't specify) - if (s_nextAutoChannel >= getRmtMaxChannels()) { + if (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) { return false; } - _channel = s_nextAutoChannel++; + + uint8_t totalBlocks; +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S3) + totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory +#elif defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32C3) // note: C6 RMT hardware is the same as C3 + totalBlocks = 4; // other supported ESP32 variants have 4 blocks +#else + totalBlocks = 4; // default to 4 if unknown, should be safe +#endif + + + + int left_channels = s_expectedChannels - s_allocatedCount - 1; + + if (left_channels == 0) { + _channel = s_currentChannelIndex; + blocksToUse = totalBlocks - s_usedBlocks; + } else { + int k = totalBlocks / s_expectedChannels; + int max_k_for_index = maxTxChannels - s_currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; + + _channel = s_currentChannelIndex; + blocksToUse = k; + } + + s_currentChannelIndex += blocksToUse; + s_usedBlocks += blocksToUse; + s_allocatedCount++; } - if (_channel >= (int8_t)getRmtMaxChannels()) { - Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, getRmtMaxChannels()); + if (_channel >= (int8_t)maxTxChannels) { + Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); return false; } _rmtChannel = (rmt_channel_t)_channel; + Serial.printf("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); + updateRmtTiming(); rmt_config_t config = {}; - + config.rmt_mode = RMT_MODE_TX; config.channel = _rmtChannel; config.gpio_num = (gpio_num_t)_pin; - config.mem_block_num = 1; + 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.carrier_freq_hz = 38000; - //config.tx_config.carrier_duty_percent = 33; config.tx_config.idle_output_en = true; config.tx_config.idle_level = _inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW; @@ -133,6 +172,19 @@ bool RmtBus::begin() { return false; } + // Register hack for memory blocks normally assigned to RX +#if defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) + #if defined(WLEDPB_ESP32S3) + for (int i = 4; i < 8; i++) { + rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); + } + #elif defined(WLEDPB_ESP32C3) + for (int i = 2; i < 4; i++) { + rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); + } + #endif +#endif + err = rmt_translator_init(_rmtChannel, translateCB); if (err != ESP_OK) { rmt_driver_uninstall(_rmtChannel); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 404ee618a8..47216f5d5f 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -38,7 +38,12 @@ class RmtBus : public IBus { void setColorOrder(ColorOrder order); // Reset the auto-allocation counter (call before re-creating buses) - static void resetAutoChannel() { s_nextAutoChannel = 0; } + static void setExpectedChannels(uint8_t expected) { s_expectedChannels = (expected > 0) ? expected : 1; } + static void resetAutoChannel() { + s_allocatedCount = 0; + s_currentChannelIndex = 0; + s_usedBlocks = 0; + } private: int8_t _pin; @@ -57,7 +62,10 @@ class RmtBus : public IBus { uint8_t* _encodeBuffer; size_t _encodeBufferSize; - static uint8_t s_nextAutoChannel; // auto-allocation counter + static uint8_t s_expectedChannels; + static uint8_t s_allocatedCount; + static uint8_t s_currentChannelIndex; + static uint8_t s_usedBlocks; static uint8_t s_activeChannelMask; // bitmask of initialized channels void updateRmtTiming(); From ed09c7b6d79d33840ee532aaf77f476e434ff8d5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 14:36:07 +0100 Subject: [PATCH 032/173] add ESP8266 support (untested) --- wled00/bus_wrapper.h | 5 + wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 22 ++ wled00/src/WLEDpixelBus/WLEDpixelBus.h | 21 +- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 332 +++++++++++++++++- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 6 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 6 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 7 +- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 6 +- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 7 +- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 6 +- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 8 +- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 2 + wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 2 +- 13 files changed, 395 insertions(+), 35 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 30650d5934..425ea2194e 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -4,10 +4,15 @@ #include "src/WLEDpixelBus/WLEDpixelBus.h" #include "src/WLEDpixelBus/WLEDpixelBus_SPI.h" + +#if defined(ARDUINO_ARCH_ESP32) #include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" #include "src/WLEDpixelBus/WLEDpixelBus_I2S.h" #include "src/WLEDpixelBus/WLEDpixelBus_LCD.h" #include "src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h" +#elif defined(ARDUINO_ARCH_ESP8266) +#include "src/WLEDpixelBus/WLEDpixelBus_ESP8266.h" +#endif //Hardware SPI Pins #define P_8266_HS_MOSI 13 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 52de8532e3..d613793f5a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -15,10 +15,17 @@ by @dedehai, 2026 -------------------------------------------------------------------------*/ #include "WLEDpixelBus.h" + +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) #include "WLEDpixelBus_RMT.h" #include "WLEDpixelBus_I2S.h" #include "WLEDpixelBus_LCD.h" #include "WLEDpixelBus_ParallelSpi.h" +#endif + +#if defined(WLEDPB_ESP8266) +#include "WLEDpixelBus_ESP8266.h" +#endif namespace WLEDpixelBus { @@ -88,6 +95,7 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, IBus* bus = nullptr; switch (type) { +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) case BusType::RMT: bus = new RmtBus(pin, timing, order, channel); break; @@ -110,6 +118,18 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, break; #endif +#elif defined(WLEDPB_ESP8266) + case BusType::UART: + bus = new Esp8266UartBus(pin, timing, order); + break; + case BusType::DMA: + bus = new Esp8266DmaBus(pin, timing, order); + break; + case BusType::BitBang: + bus = new Esp8266BitBangBus(pin, timing, order); + break; +#endif + default: return nullptr; } @@ -124,6 +144,8 @@ BusType getRecommendedBusType() { return BusType::I2S; // Original and S2 have I2S parallel #elif defined(WLEDPB_SPI_SUPPORT) return BusType::SPI; // C3 uses SPI quad mode +#elif defined(WLEDPB_ESP8266) + return BusType::UART; #else return BusType::RMT; // Fallback to RMT #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 89767c36a8..61f9d7f47e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -19,9 +19,7 @@ by @dedehai, 2026 #include #include -#if ! defined(ARDUINO_ARCH_ESP32) -#error "WLEDpixelBus only supports ESP32 platforms" -#endif +#if defined(ARDUINO_ARCH_ESP32) #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -65,6 +63,14 @@ by @dedehai, 2026 #include "rom/lldesc.h" #endif +#elif defined(ARDUINO_ARCH_ESP8266) + +#define WLEDPB_ESP8266 + +#else +#error "WLEDpixelBus only supports ESP32 and ESP8266 platforms" +#endif + #include "WLEDpixelBus_Timings.h" namespace WLEDpixelBus { @@ -312,6 +318,9 @@ enum class BusType : uint8_t { I2S = 1, LCD = 2, SPI = 3, + UART = 4, + DMA = 5, + BitBang = 6, Auto = 255 }; @@ -375,7 +384,13 @@ static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t ch } // namespace WLEDpixelBus +#include "WLEDpixelBus_SPI.h" + +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) #include "WLEDpixelBus_RMT.h" #include "WLEDpixelBus_I2S.h" #include "WLEDpixelBus_LCD.h" #include "WLEDpixelBus_ParallelSpi.h" +#elif defined(WLEDPB_ESP8266) +#include "WLEDpixelBus_ESP8266.h" +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index 946a326d29..387dc734bb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -1,4 +1,328 @@ -#include "WLEDpixelBus.h" -#ifdef WLEDPB_ESP8266 - -#endif +#include "WLEDpixelBus_ESP8266.h" + +#ifdef WLEDPB_ESP8266 + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ARDUINO_ESP8266_MAJOR +#include +#else +#include +#endif + +namespace WLEDpixelBus { + +//============================================================================== +// ESP8266 UART Bus +//============================================================================== + +Esp8266UartBus::Esp8266UartBus(int8_t pin, const LedTiming& timing, ColorOrder order) + : _pin(pin), _timing(timing), _order(order), _initialized(false), _encodeLut(nullptr), _encodeBuffer(nullptr), _encodeBufferSize(0) {} + +Esp8266UartBus::~Esp8266UartBus() { + end(); + if (_encodeLut) free(_encodeLut); + if (_encodeBuffer) free(_encodeBuffer); +} + +void Esp8266UartBus::buildLut() { + if (!_encodeLut) _encodeLut = (uint8_t*)malloc(256 * 4); + if (!_encodeLut) return; + const uint8_t uartData[4] = {0b110111, 0b000111, 0b110100, 0b000100}; + for (int i=0; i<256; i++) { + _encodeLut[i*4 + 0] = uartData[(i >> 6) & 0x03]; + _encodeLut[i*4 + 1] = uartData[(i >> 4) & 0x03]; + _encodeLut[i*4 + 2] = uartData[(i >> 2) & 0x03]; + _encodeLut[i*4 + 3] = uartData[i & 0x03]; + } +} + +bool Esp8266UartBus::allocateBuffer(size_t encodedDataLen) { + if (_encodeBufferSize >= encodedDataLen) return true; + if (_encodeBuffer) free(_encodeBuffer); + _encodeBuffer = (uint8_t*)malloc(encodedDataLen); + if (!_encodeBuffer) return false; + _encodeBufferSize = encodedDataLen; + return true; +} + +bool Esp8266UartBus::begin() { + if (_initialized) return true; + if (_pin != 1 && _pin != 2) return false; + buildLut(); + updateUartTiming(); + _initialized = true; + return true; +} + +void Esp8266UartBus::end() { + if (!_initialized) return; + 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; + + if (_pin == 2) { + Serial1.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); + USC0(1) |= (1 << UCTXI); // set inverted logic + } else { + Serial.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); + USC0(0) |= (1 << UCTXI); // set inverted logic + } +} + +bool Esp8266UartBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } + if (!_initialized || !pixels || numPixels == 0) return false; + + size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); + size_t outLen = numPixels * bpp * 4; + if (!allocateBuffer(outLen)) return false; + + ColorEncoder encoder(_order); + uint8_t* out = _encodeBuffer; + uint8_t pData[5]; + + for (size_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); + for (size_t b = 0; b < bpp; b++) { + uint8_t v = pData[b]; + *out++ = _encodeLut[v * 4 + 0]; + *out++ = _encodeLut[v * 4 + 1]; + *out++ = _encodeLut[v * 4 + 2]; + *out++ = _encodeLut[v * 4 + 3]; + } + } + + uint8_t uartNum = (_pin == 2) ? 1 : 0; + out = _encodeBuffer; + size_t len = outLen; + + // Busy wait UART filling to ensure stable rendering (OS interrupts intact but tight timing loop) + while(len > 0) { + if (((USS(uartNum) >> USTXC) & 0xff) < 127) { + USF(uartNum) = *out++; + len--; + } + } + return true; +} + +bool Esp8266UartBus::canShow() const { + if (!_initialized) return false; + uint8_t uartNum = (_pin == 2) ? 1 : 0; + return (((USS(uartNum) >> USTXC) & 0xff) == 0); // ready when FIFO empty +} + +void Esp8266UartBus::waitComplete() { + while (!canShow()) { yield(); } +} + + +//============================================================================== +// ESP8266 BitBang Bus +//============================================================================== + +Esp8266BitBangBus::Esp8266BitBangBus(int8_t pin, const LedTiming& timing, ColorOrder order) + : _pin(pin), _timing(timing), _order(order), _initialized(false) {} + +Esp8266BitBangBus::~Esp8266BitBangBus() { + end(); +} + +bool Esp8266BitBangBus::begin() { + if (_initialized) return true; + pinMode(_pin, OUTPUT); + digitalWrite(_pin, LOW); + setTiming(_timing); + _initialized = true; + return true; +} + +void Esp8266BitBangBus::end() { + pinMode(_pin, INPUT); + _initialized = false; +} + +bool Esp8266BitBangBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } + if (!_initialized || !pixels || numPixels == 0) return false; + + size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); + ColorEncoder encoder(_order); + uint32_t mask = 1 << _pin; + uint8_t pData[5]; + + os_intr_lock(); + for (size_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); + for (size_t b = 0; b < bpp; b++) { + uint8_t v = pData[b]; + for (int bit = 7; bit >= 0; bit--) { + uint32_t t = ESP.getCycleCount(); + if (v & (1 << bit)) { + GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, mask); + while((ESP.getCycleCount() - t) < _t1h); + GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, mask); + while((ESP.getCycleCount() - t) < (_t1h + _t1l)); + } else { + GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, mask); + while((ESP.getCycleCount() - t) < _t0h); + GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, mask); + while((ESP.getCycleCount() - t) < (_t0h + _t0l)); + } + } + } + } + os_intr_unlock(); + return true; +} + +bool Esp8266BitBangBus::canShow() const { + return _initialized; +} + +void Esp8266BitBangBus::waitComplete() { + // Blocking show, always complete upon return +} + + +//============================================================================== +// ESP8266 DMA Bus +//============================================================================== + +Esp8266DmaBus::Esp8266DmaBus(int8_t pin, const LedTiming& timing, ColorOrder order) + : _pin(pin), _timing(timing), _order(order), _initialized(false), _encodeBuffer(nullptr), _encodeBufferSize(0) {} + +Esp8266DmaBus::~Esp8266DmaBus() { + end(); + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + } +} + +bool Esp8266DmaBus::allocateBuffer(size_t len) { + if (_encodeBufferSize >= len) return true; + if (_encodeBuffer) free(_encodeBuffer); + _encodeBuffer = (uint8_t*)malloc(len); + if (!_encodeBuffer) return false; + _encodeBufferSize = len; + return true; +} + +void Esp8266DmaBus::updateI2sTiming() { + // Setup using Native ESP8266 Core I2S + uint32_t bitPeriod = _timing.bitPeriod(); + if (bitPeriod == 0) bitPeriod = 1250; + + // We map 4 I2S bits to 1 LED bit (4-step cadence). + // Each LED bit needs 4 I2S periods, thus our desired I2S clock is 4x the LED bit rate. + // periodNs is time per LED bit. I2S bit clock period = bitPeriod / 4. + // I2S bits/sec = 1,000,000,000 / (bitPeriod / 4) = 4,000,000,000 / bitPeriod. + // Using core `i2s_begin` which sets a clock and routing: + // Actually, `i2s_begin` expects a sample rate for 32-bit (stereo 16+16) frames. + // Sample rate = (I2S bits/sec) / 32 + uint32_t sampleRate = (4000000000ULL / bitPeriod) / 32; + i2s_set_rate(sampleRate); +} + +bool Esp8266DmaBus::begin() { + if (_initialized) return true; + if (_pin != 3) return false; // ESP8266 I2S RX is GPIO3 for NeoPixels + + // Begin core I2S subsystem + i2s_begin(); + + // To make GPIO3 act as I2S Data Out instead of I2S IN, configure registers manually: + // (This disables I2S BCLK and WS on their default pins so we don't interfere with other hardware) + pinMode(3, FUNCTION_3); // Set RX to I2S0_DATA + + // Disable clock/ws pins on GPIO15/2 + PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15); + PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2); + + updateI2sTiming(); + + _initialized = true; + return true; +} + +void Esp8266DmaBus::end() { + if (!_initialized) return; + i2s_end(); + pinMode(_pin, INPUT); + _initialized = false; +} + +bool Esp8266DmaBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { + if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } + if (!_initialized || !pixels || numPixels == 0) return false; + + size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); + + // 4 step cadence per LED bit. + // 1 LED bit = 4 I2S bits (half-byte / nibble). + // 1 LED byte (8 bits) = 32 I2S bits (4 bytes). + size_t outLen = numPixels * bpp * 4 + 40; // + 40 zero bytes for reset (latch) + if (!allocateBuffer(outLen)) return false; + + memset(_encodeBuffer, 0, outLen); + + ColorEncoder encoder(_order); + uint32_t* out32 = (uint32_t*)_encodeBuffer; + uint8_t pData[5]; + + // Using inverted 4-step cadence: + // Normally: + // 1 = 0b1110 (0xE) + // 0 = 0b1000 (0x8) + for (size_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); + for (size_t b = 0; b < bpp; b++) { + uint32_t i2sData = 0; + uint8_t v = pData[b]; + for (int bit = 7; bit >= 0; bit--) { + i2sData <<= 4; + if (v & (1 << bit)) { + i2sData |= 0xE; // 1110 + } else { + i2sData |= 0x8; // 1000 + } + } + *out32++ = i2sData; + } + } + + // Write using Core I2S: It processes full `uint32` buffer natively over I2S + uint32_t* buf32 = (uint32_t*)_encodeBuffer; + for (size_t i = 0; i < outLen / 4; i++) { + i2s_write_sample(buf32[i]); + } + + return true; +} + +bool Esp8266DmaBus::canShow() const { + return _initialized && !i2s_is_full(); +} + +void Esp8266DmaBus::waitComplete() { + // Not strictly supported blocking via I2S, wait for FIFO room + while (i2s_is_full()) { yield(); } +} + +} // namespace WLEDpixelBus + +#endif // WLEDPB_ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index 1d22a7176e..c71de242f4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -18,7 +18,7 @@ class Esp8266UartBus : public IBus { bool begin() override; void end() override; - bool show() override; + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; bool canShow() const override; void waitComplete() override; const char* getType() const override { return "ESP8266_UART"; } @@ -53,7 +53,7 @@ class Esp8266DmaBus : public IBus { bool begin() override; void end() override; - bool show() override; + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; bool canShow() const override; void waitComplete() override; const char* getType() const override { return "ESP8266_DMA"; } @@ -86,7 +86,7 @@ class Esp8266BitBangBus : public IBus { bool begin() override; void end() override; - bool show() override; + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; bool canShow() const override; void waitComplete() override; const char* getType() const override { return "ESP8266_BB"; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 5473aa765c..52045b7527 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -1,4 +1,5 @@ #include "WLEDpixelBus.h" +#ifdef WLEDPB_I2S_SUPPORT #include "WLEDpixelBus_I2S.h" namespace WLEDpixelBus { @@ -7,8 +8,6 @@ namespace WLEDpixelBus { // I2S Bus Implementation //============================================================================== -#ifdef WLEDPB_I2S_SUPPORT - I2sBusContext* I2sBusContext::_instances[WLEDPB_I2S_BUS_COUNT] = {nullptr}; uint8_t I2sBusContext::_refCount[WLEDPB_I2S_BUS_COUNT] = {0}; @@ -661,6 +660,7 @@ void I2sBus::setColorOrder(ColorOrder order) { _order = order; } -#endif // WLEDPB_I2S_SUPPORT + } // namespace WLEDpixelBus +#endif // WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index a3f47d46b0..355006a9e1 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -1,15 +1,13 @@ #pragma once #include "WLEDpixelBus.h" - +#ifdef WLEDPB_I2S_SUPPORT namespace WLEDpixelBus { //============================================================================== // I2S Parallel Bus - ESP32 and ESP32-S2 //============================================================================== -#ifdef WLEDPB_I2S_SUPPORT - #include "soc/i2s_struct.h" #include "soc/i2s_reg.h" #include "driver/periph_ctrl.h" @@ -142,6 +140,5 @@ class I2sBus : public IBus { bool allocateBuffer(uint16_t numPixels); }; -#endif // WLEDPB_I2S_SUPPORT - } // namespace WLEDpixelBus +#endif // WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index f42308b3f1..41cde72fb3 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -1,4 +1,5 @@ #include "WLEDpixelBus.h" +#ifdef WLEDPB_LCD_SUPPORT #include "WLEDpixelBus_LCD.h" namespace WLEDpixelBus { @@ -16,8 +17,6 @@ Key design: - No DMA reconfiguration during transmission -------------------------------------------------------------------------*/ -#ifdef WLEDPB_LCD_SUPPORT - #include "driver/periph_ctrl.h" #include "esp_private/gdma.h" #include "esp_rom_gpio.h" @@ -578,6 +577,5 @@ void LcdBus::setColorOrder(ColorOrder order) { _order = order; } -#endif // WLEDPB_LCD_SUPPORT - } // namespace WLEDpixelBus +#endif \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index 6abe6073e2..dec3a780e4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -1,5 +1,5 @@ #pragma once - +#ifdef WLEDPB_LCD_SUPPORT #include "WLEDpixelBus.h" namespace WLEDpixelBus { @@ -7,7 +7,6 @@ namespace WLEDpixelBus { //============================================================================== // LCD Parallel Bus - ESP32-S3 only //============================================================================== -#ifdef WLEDPB_LCD_SUPPORT #include "driver/periph_ctrl.h" #include "esp_private/gdma.h" @@ -145,6 +144,6 @@ class LcdBus : public IBus { size_t _encodedLen; }; -#endif // WLEDPB_LCD_SUPPORT - } // namespace WLEDpixelBus + +#endif // WLEDPB_LCD_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 411151de0a..664f38f3fe 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -1,14 +1,13 @@ #include "WLEDpixelBus.h" +#ifdef WLEDPB_SPI_SUPPORT #include "WLEDpixelBus_ParallelSpi.h" -#ifdef WLEDPB_SPI_SUPPORT #undef FLAG_ATTR #define FLAG_ATTR(TYPE) #include "hal/spi_ll.h" #include "driver/periph_ctrl.h" #include "esp_private/gdma.h" #include "esp_rom_gpio.h" -#endif namespace WLEDpixelBus { @@ -16,7 +15,6 @@ namespace WLEDpixelBus { // SPI Parallel Bus Implementation (ESP32-C3) //============================================================================== -#ifdef WLEDPB_SPI_SUPPORT // low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) static inline void spi_ll_apply_config(spi_dev_t *hw) @@ -624,6 +622,6 @@ void ParallelSpiBus::setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp) IBus::setPixelColor(pix, c, cp); } -#endif // WLEDPB_SPI_SUPPORT } // namespace WLEDpixelBus +#endif // WLEDPB_SPI_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index 28d58210a0..b32f7c2dd4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -1,13 +1,13 @@ #pragma once #include "WLEDpixelBus.h" - +#ifdef WLEDPB_SPI_SUPPORT namespace WLEDpixelBus { //============================================================================== // SPI Parallel Bus - ESP32-C3 (uses SPI2 quad mode + GDMA) //============================================================================== -#ifdef WLEDPB_SPI_SUPPORT + #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines #define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 @@ -120,6 +120,6 @@ class ParallelSpiBus : public IBus { size_t _encodeBufferSize; }; -#endif // WLEDPB_SPI_SUPPORT - } // namespace WLEDpixelBus + +#endif // WLEDPB_SPI_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 186fc01aeb..4f374022c5 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -1,4 +1,5 @@ #include "WLEDpixelBus.h" +#ifdef ARDUINO_ARCH_ESP32 #include "WLEDpixelBus_RMT.h" namespace WLEDpixelBus { @@ -347,3 +348,4 @@ void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, } } // namespace WLEDpixelBus +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp index ef09439238..18138fe0dd 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -1,4 +1,4 @@ -#include "WLEDpixelBus_SPI.h" +#include "WLEDpixelBus.h" namespace WLEDpixelBus { From 959b69bda20f2e4876c5554ad5673a31b1a363d4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 14:36:31 +0100 Subject: [PATCH 033/173] fix file access glitch on C3 --- wled00/file.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wled00/file.cpp b/wled00/file.cpp index dcc2d57c41..cfbda0df1e 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -436,6 +436,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; From 780fdeb89a57c41059e1936da8e6213b481f38b2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 15:19:25 +0100 Subject: [PATCH 034/173] use all 16 available LCD channels on S3, optimize speed in 4stepencoding --- wled00/const.h | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 3 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 43 +++++++++------ wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 58 ++++++++++++-------- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 8 +-- 5 files changed, 67 insertions(+), 47 deletions(-) diff --git a/wled00/const.h b/wled00/const.h index 295e7872ae..c8a427f953 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -77,7 +77,7 @@ constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_C #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels - #define WLED_MAX_I2S_CHANNELS 8 // uses LCD parallel output not I2S + #define WLED_MAX_I2S_CHANNELS 16 // uses LCD parallel output not I2S and supports up to 16 parallel channels //#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 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 61f9d7f47e..3ace67be29 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -362,6 +362,7 @@ BusType getRecommendedBusType(); * Estimate exact memory footprint of a bus. * Accounts for encode buffers, driver overhead, and shared DMA contexts. */ + // TODO: check if this is accurate and busmanager does not double-account for things calculated here static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t channelCount, size_t dmaBufferSize = DEFAULT_DMA_BUFFER_SIZE) { size_t mem = 0; @@ -376,7 +377,7 @@ static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t ch if (type == BusType::I2S || type == BusType::LCD || type == BusType::Auto) { mem += dmaBufferSize * 2; // Include minimal descriptor memory estimates (~64 bytes per context) - mem += 64; + mem += 64; } return mem; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 52045b7527..c3d5f24104 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -371,11 +371,17 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { memset(dest, 0, destLen); size_t pos = 0; + // Pre-calculate max channels to speed up loop + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) maxCh = ch + 1; + } + // Process each source byte position across all channels while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte bool hasData = false; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + for (int ch = 0; ch < maxCh; ch++) { if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; @@ -384,29 +390,30 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { uint8_t chMask = (1 << ch); uint8_t* p = dest + pos; - for (int bit = 7; bit >= 0; bit--) { - uint8_t dataVal = (srcByte >> bit) & 1; - - // Half-word swapped: memory layout [step2, step3, step0, step1] - // Step 0 (HIGH) -> p[2] - p[2] |= chMask; - // Step 1 (data) -> p[3] - if (dataVal) p[3] |= chMask; - - // Step 2 (data) -> p[0] - if (dataVal) p[0] |= chMask; - - // Step 3 (LOW) -> p[1] (already 0) - - p += 4; - } + // Half-word swapped: memory layout [step2, step3, step0, step1] + // bit 7 + p[2] |= chMask; if (srcByte & 0x80) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 6 + p[2] |= chMask; if (srcByte & 0x40) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 5 + p[2] |= chMask; if (srcByte & 0x20) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 4 + p[2] |= chMask; if (srcByte & 0x10) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 3 + p[2] |= chMask; if (srcByte & 0x08) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 2 + p[2] |= chMask; if (srcByte & 0x04) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 1 + p[2] |= chMask; if (srcByte & 0x02) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 0 + p[2] |= chMask; if (srcByte & 0x01) { p[3] |= chMask; p[0] |= chMask; } p += 4; } if (!hasData) break; // Advance all channel positions - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + for (int ch = 0; ch < maxCh; ch++) { if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { _channels[ch].srcPos++; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 41cde72fb3..91135900ba 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -172,7 +172,7 @@ bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16B 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; - LCD_CAM.lcd_user.lcd_2byte_en = use16Bit ? 1 : 0; + LCD_CAM.lcd_user.lcd_2byte_en = 1; // Always use 16-bit output for 16 channels LCD_CAM.lcd_user.lcd_dummy = 1; LCD_CAM.lcd_user.lcd_dummy_cyclelen = 0; LCD_CAM.lcd_user.lcd_cmd = 0; @@ -266,7 +266,7 @@ int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus) { gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); - LCD_LOG("Channel %d: pin=%d, mask=0x%02X", idx, pin, _channelMask); + LCD_LOG("Channel %d: pin=%d, mask=0x%04X", idx, pin, _channelMask); return idx; } @@ -299,50 +299,62 @@ void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { // 4-step cadence encoding for parallel output without byte swapping - // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) + // Each source bit becomes 4 DMA words (one bit per channel in each 16-bit word) // Desired output: [HIGH][data][data][LOW] for each bit // Buffer is always filled completely (zeros = LOW = reset signal) memset(dest, 0, destLen); size_t pos = 0; + // Pre-calculate max channels to speed up loop + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) maxCh = ch + 1; + } + // Process each source byte position across all channels - while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte + while (pos + 64 <= destLen) { // 8 bits * 4 steps * 2 bytes = 64 bytes per source byte bool hasData = false; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + for (int ch = 0; ch < maxCh; ch++) { if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; hasData = true; uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; - uint8_t chMask = (1 << ch); - - uint8_t* p = dest + pos; - for (int bit = 7; bit >= 0; bit--) { - uint8_t dataVal = (srcByte >> bit) & 1; - - // Step 0 (HIGH) - p[0] |= chMask; - // Step 1 (data) - if (dataVal) p[1] |= chMask; - // Step 2 (data) - if (dataVal) p[2] |= chMask; - // Step 3 (LOW) - already 0 - p += 4; - } + uint16_t chMask = (1 << ch); + + uint16_t* p = (uint16_t*)(dest + pos); + + // Unrolled loop for 8 bits + // bit 7 + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 6 + p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 5 + p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 4 + p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 3 + p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 2 + p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 1 + p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 0 + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; } if (!hasData) break; // Advance all channel positions - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + for (int ch = 0; ch < maxCh; ch++) { if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { _channels[ch].srcPos++; } } - pos += 32; + pos += 64; } // Rest of buffer remains zero (reset signal) from memset } @@ -438,7 +450,7 @@ IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, } void LcdBusContext::printDebugStats() { - LCD_LOG("state=%u, channels=%u, mask=0x%02X", (unsigned)_state, _channelCount, _channelMask); + LCD_LOG("state=%u, channels=%u, mask=0x%04X", (unsigned)_state, _channelCount, _channelMask); } // ============================================ diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index dec3a780e4..a382e94f5c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -18,7 +18,7 @@ namespace WLEDpixelBus { #include "soc/gpio_sig_map.h" #ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE 1024 //2048 -> 2048 works well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) +#define WLEDPB_LCD_DMA_BUFFER_SIZE 2048 //2048 -> 2048 works well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) #endif #ifndef WLEDPB_LCD_CADENCE_STEPS @@ -36,7 +36,7 @@ static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); -#define WLEDPB_LCD_MAX_CHANNELS 8 +#define WLEDPB_LCD_MAX_CHANNELS 16 class LcdBusContext { public: @@ -98,8 +98,8 @@ class LcdBusContext { }; ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; uint8_t _channelCount; - uint8_t _channelMask; - uint8_t _stagedMask; + uint16_t _channelMask; + uint16_t _stagedMask; size_t _maxDataLen; static LcdBusContext* _instance; From 669221cf68a1b40b4ddc826907a94ee35963f6c9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 15:57:30 +0100 Subject: [PATCH 035/173] minor optimizations for LCD driver --- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 91135900ba..7f6299e549 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -396,6 +396,7 @@ bool LcdBusContext::startTransmit() { 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); @@ -446,7 +447,7 @@ IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, ctx->_state = DriverState::Idle; } - return true; + return false; // Do not yield OS for this DMA streaming interrupt } void LcdBusContext::printDebugStats() { @@ -547,7 +548,7 @@ bool LcdBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc _ctx->forceIdle(); break; } - delay(1); + yield(); } if (!allocateBuffer(_numPixels)) return false; @@ -581,7 +582,7 @@ void LcdBus::waitComplete() { _ctx->forceIdle(); return; } - delay(1); + yield(); } } From 303ef48f16387c1b489bdc18afa4aa6af1036f0b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 18:04:58 +0100 Subject: [PATCH 036/173] Fix output on ESP8266 --- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 41 +++++++++++++------ .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 2 +- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index 387dc734bb..b932a3eae4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -73,13 +73,26 @@ void Esp8266UartBus::updateUartTiming() { if (periodNs < 200) periodNs = 1250; uint32_t baud = 4000000000ULL / periodNs; - if (_pin == 2) { + uint8_t uartNum = (_pin == 2) ? 1 : 0; + if (uartNum == 1) { Serial1.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); - USC0(1) |= (1 << UCTXI); // set inverted logic } else { Serial.begin(baud, SERIAL_6N1, SERIAL_TX_ONLY); - USC0(0) |= (1 << UCTXI); // set inverted logic } + + // Clear the RX & TX FIFOS + const uint32_t fifoResetFlags = (1 << UCTXRST) | (1 << UCRXRST); + USC0(uartNum) |= fifoResetFlags; + USC0(uartNum) &= ~(fifoResetFlags); + + // clear all invert bits + USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI)); + + // set inverted logic for TX + USC0(uartNum) |= (1 << UCTXI); + + // Disable RX & TX interrupts that might have been enabled by Arduino Core Serial + USIE(uartNum) &= ~((1 << UIFF) | (1 << UIFE)); } bool Esp8266UartBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { @@ -155,6 +168,15 @@ void Esp8266BitBangBus::end() { _initialized = false; } +void Esp8266BitBangBus::setTiming(const LedTiming& timing) { + _timing = timing; + uint32_t cpuFreq = ESP.getCpuFreqMHz(); // usually 80 or 160 + _t0h = (timing.t0h_ns * cpuFreq) / 1000; + _t0l = (timing.t0l_ns * cpuFreq) / 1000; + _t1h = (timing.t1h_ns * cpuFreq) / 1000; + _t1l = (timing.t1l_ns * cpuFreq) / 1000; +} + bool Esp8266BitBangBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } if (!_initialized || !pixels || numPixels == 0) return false; @@ -242,16 +264,11 @@ bool Esp8266DmaBus::begin() { if (_initialized) return true; if (_pin != 3) return false; // ESP8266 I2S RX is GPIO3 for NeoPixels - // Begin core I2S subsystem - i2s_begin(); - + // Begin core I2S subsystem without driving clocks on GPIO15/GPIO2! + i2s_rxtxdrive_begin(false, true, false, false); + // To make GPIO3 act as I2S Data Out instead of I2S IN, configure registers manually: - // (This disables I2S BCLK and WS on their default pins so we don't interfere with other hardware) - pinMode(3, FUNCTION_3); // Set RX to I2S0_DATA - - // Disable clock/ws pins on GPIO15/2 - PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15); - PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2); + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, 1); // Select I2SO_DATA on GPIO3 updateI2sTiming(); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index c71de242f4..23167e3e98 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -91,7 +91,7 @@ class Esp8266BitBangBus : public IBus { void waitComplete() override; const char* getType() const override { return "ESP8266_BB"; } - void setTiming(const LedTiming& timing) { _timing = timing; } + void setTiming(const LedTiming& timing); void setColorOrder(ColorOrder order) { _order = order; } private: From 0cfad467222dc5cb74bab906ed5785f650d47bb8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 13 Mar 2026 20:34:41 +0100 Subject: [PATCH 037/173] attempt at fixing I2S on S2 but its not yet working --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index c3d5f24104..7dbc237795 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -315,6 +315,8 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { int sigIdx; #if defined(WLEDPB_ESP32) sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; +#elif defined(WLEDPB_ESP32S2) + sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes #else sigIdx = I2S0O_DATA_OUT0_IDX; #endif @@ -391,6 +393,25 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { uint8_t* p = dest + pos; +#if defined(WLEDPB_ESP32S2) + // ESP32-S2 does NOT swap half-words (memory layout [step0, step1, step2, step3]) + // bit 7 + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 6 + p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 5 + p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 4 + p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 3 + p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 2 + p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 1 + p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 0 + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; +#else // Half-word swapped: memory layout [step2, step3, step0, step1] // bit 7 p[2] |= chMask; if (srcByte & 0x80) { p[3] |= chMask; p[0] |= chMask; } p += 4; @@ -408,6 +429,8 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { p[2] |= chMask; if (srcByte & 0x02) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 0 p[2] |= chMask; if (srcByte & 0x01) { p[3] |= chMask; p[0] |= chMask; } p += 4; +#endif + } if (!hasData) break; From 482e0e20e267fd03bcba2610f6fe4bbafb8d122a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 14 Mar 2026 07:04:23 +0100 Subject: [PATCH 038/173] speed optimizations in setpixelcolor and color order ( about 30% faster) --- wled00/bus_manager.cpp | 54 +++++---- wled00/bus_manager.h | 2 + wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 147 ++++++++++++++++------- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 30 ++++- 4 files changed, 167 insertions(+), 66 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 2b3cb91ed8..bbeda5430b 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -163,8 +163,10 @@ BusDigital::BusDigital(const BusConfig &bc) // fix for wled#4759 if (_valid) { _busPtr->allocatePixelBuffer(lenToCreate + _skip, _hasCCT); + _pixelDataPtr = _busPtr->getPixelData(); + _cctDataPtr = _busPtr->getCctData(); for (unsigned i = 0; i < _skip; i++) { - _busPtr->setPixelColor(i, 0); // set sacrificial pixels to black (color order does not matter here) + if (_pixelDataPtr) _pixelDataPtr[i] = 0; // set sacrificial pixels to black } } else { cleanup(); @@ -242,14 +244,13 @@ void BusDigital::applyBriLimit(uint8_t newBri) { } 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 - if (hasCCT()) { - WLEDpixelBus::CctPixel cp; - cp.ww = wwcw & 0xFF; - cp.cw = wwcw >> 8; - _busPtr->setPixelColor(i, c, &cp); - } else { - _busPtr->setPixelColor(i, c); - } + if (hasCCT() && _cctDataPtr) { + _cctDataPtr[i].ww = wwcw & 0xFF; + _cctDataPtr[i].cw = wwcw >> 8; + } + if (_pixelDataPtr) { + _pixelDataPtr[i] = c; + } } } @@ -259,7 +260,7 @@ 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()) - _busPtr->show(); + _busPtr->show(); } bool BusDigital::canShow() const { @@ -315,14 +316,13 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { } } - if (hasCCT()) { - WLEDpixelBus::CctPixel cp; - cp.ww = wwcw & 0xFF; - cp.cw = wwcw >> 8; - _busPtr->setPixelColor(pix, c, &cp); - } else { - _busPtr->setPixelColor(pix, c); - } + if (hasCCT() && _cctDataPtr) { + _cctDataPtr[pix].ww = wwcw & 0xFF; + _cctDataPtr[pix].cw = wwcw >> 8; + } + if (_pixelDataPtr) { + _pixelDataPtr[pix] = c; + } } // returns lossly restored color from bus @@ -330,8 +330,8 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; if (_reversed) pix = _len - pix -1; pix += _skip; - const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - uint32_t rawC = _busPtr->getPixelColor((_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix); + const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); // TODO: do we need the color order? where is getpixelcolor used? + uint32_t rawC = _busPtr->getPixelColor((_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix); // TODO: check if this is correct uint32_t c = restoreColorLossy(rawC, _NPBbri); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs uint8_t r = R(c); @@ -412,6 +412,8 @@ void BusDigital::cleanup() { } _valid = false; _busPtr = nullptr; + _pixelDataPtr = nullptr; + _cctDataPtr = nullptr; PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -1351,9 +1353,17 @@ void BusManager::show() { } void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { + static Bus* lastBus = nullptr; + if (lastBus && lastBus->containsPixel(pix)) { + lastBus->setPixelColor(pix - lastBus->getStart(), c); + return; + } for (auto &bus : busses) { - if (!bus->containsPixel(pix)) continue; - bus->setPixelColor(pix - bus->getStart(), c); + if (bus->containsPixel(pix)) { + bus->setPixelColor(pix - bus->getStart(), c); + lastBus = bus.get(); + return; + } } } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 863a89b4ec..5ae5d6edc8 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -284,6 +284,8 @@ class BusDigital : public Bus { uint16_t _milliAmpsLimit; uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current WLEDpixelBus::IBus* _busPtr; + uint32_t* _pixelDataPtr = nullptr; + WLEDpixelBus::CctPixel* _cctDataPtr = nullptr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index d613793f5a..57d98427e9 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -33,48 +33,111 @@ namespace WLEDpixelBus { // Color Encoder Implementation //============================================================================== -void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const { - uint8_t r = getR(pixel); - uint8_t g = getG(pixel); - uint8_t b = getB(pixel); - uint8_t w = getW(pixel); - - // For CCT strips, use CCT data instead of W channel - uint8_t ww = cct ? cct->ww : w; - uint8_t cw = cct ? cct->cw : 0; - - switch (_order) { - // RGB variants - case ColorOrder::RGB: out[0]=r; out[1]=g; out[2]=b; break; - case ColorOrder::GRB: out[0]=g; out[1]=r; out[2]=b; break; - case ColorOrder::BRG: out[0]=b; out[1]=r; out[2]=g; break; - case ColorOrder::RBG: out[0]=r; out[1]=b; out[2]=g; break; - case ColorOrder::GBR: out[0]=g; out[1]=b; out[2]=r; break; - case ColorOrder::BGR: out[0]=b; out[1]=g; out[2]=r; break; - - // RGBW variants - case ColorOrder::RGBW: out[0]=r; out[1]=g; out[2]=b; out[3]=w; break; - case ColorOrder::GRBW: out[0]=g; out[1]=r; out[2]=b; out[3]=w; break; - case ColorOrder::BRGW: out[0]=b; out[1]=r; out[2]=g; out[3]=w; break; - case ColorOrder::RBGW: out[0]=r; out[1]=b; out[2]=g; out[3]=w; break; - case ColorOrder::GBRW: out[0]=g; out[1]=b; out[2]=r; out[3]=w; break; - case ColorOrder::BGRW: out[0]=b; out[1]=g; out[2]=r; out[3]=w; break; - case ColorOrder::WRGB: out[0]=w; out[1]=r; out[2]=g; out[3]=b; break; - case ColorOrder::WGRB: out[0]=w; out[1]=g; out[2]=r; out[3]=b; break; - case ColorOrder::WBRG: out[0]=w; out[1]=b; out[2]=r; out[3]=g; break; - case ColorOrder::WRBG: out[0]=w; out[1]=r; out[2]=b; out[3]=g; break; - case ColorOrder::WGBR: out[0]=w; out[1]=g; out[2]=b; out[3]=r; break; - case ColorOrder::WBGR: out[0]=w; out[1]=b; out[2]=g; out[3]=r; break; - - // RGBWC variants - case ColorOrder::RGBWC: out[0]=r; out[1]=g; out[2]=b; out[3]=ww; out[4]=cw; break; - case ColorOrder::GRBWC: out[0]=g; out[1]=r; out[2]=b; out[3]=ww; out[4]=cw; break; - case ColorOrder::BRGWC: out[0]=b; out[1]=r; out[2]=g; out[3]=ww; out[4]=cw; break; - case ColorOrder::RBGWC: out[0]=r; out[1]=b; out[2]=g; out[3]=ww; out[4]=cw; break; - case ColorOrder::GBRWC: out[0]=g; out[1]=b; out[2]=r; out[3]=ww; out[4]=cw; break; - case ColorOrder::BGRWC: out[0]=b; out[1]=g; out[2]=r; out[3]=ww; out[4]=cw; break; - - default: out[0]=g; out[1]=r; out[2]=b; break; +ColorEncoder::ColorEncoder(ColorOrder order) : _order(order), _numChannels(getChannelCount(order)) +{ + _idxR = _idxG = _idxB = _idxW = _idxWW = _idxCW = 0xFF; +//TODO: is there a better way to define color orders indices than just this dumb list? it also does not cover all possibilities I think (swapping white and color channels) + switch(order) + { + case ColorOrder::RGB: + _idxR=0; _idxG=1; _idxB=2; + break; + + case ColorOrder::GRB: + _idxG=0; _idxR=1; _idxB=2; + break; + + case ColorOrder::BRG: + _idxB=0; _idxR=1; _idxG=2; + break; + + case ColorOrder::RBG: + _idxR=0; _idxB=1; _idxG=2; + break; + + case ColorOrder::GBR: + _idxG=0; _idxB=1; _idxR=2; + break; + + case ColorOrder::BGR: + _idxB=0; _idxG=1; _idxR=2; + break; + + case ColorOrder::RGBW: + _idxR=0; _idxG=1; _idxB=2; _idxW=3; + break; + + case ColorOrder::GRBW: + _idxG=0; _idxR=1; _idxB=2; _idxW=3; + break; + + case ColorOrder::BRGW: + _idxB=0; _idxR=1; _idxG=2; _idxW=3; + break; + + case ColorOrder::RBGW: + _idxR=0; _idxB=1; _idxG=2; _idxW=3; + break; + + case ColorOrder::GBRW: + _idxG=0; _idxB=1; _idxR=2; _idxW=3; + break; + + case ColorOrder::BGRW: + _idxB=0; _idxG=1; _idxR=2; _idxW=3; + break; + + case ColorOrder::WRGB: + _idxW=0; _idxR=1; _idxG=2; _idxB=3; + break; + + case ColorOrder::WGRB: + _idxW=0; _idxG=1; _idxR=2; _idxB=3; + break; + + case ColorOrder::WBRG: + _idxW=0; _idxB=1; _idxR=2; _idxG=3; + break; + + case ColorOrder::WRBG: + _idxW=0; _idxR=1; _idxB=2; _idxG=3; + break; + + case ColorOrder::WGBR: + _idxW=0; _idxG=1; _idxB=2; _idxR=3; + break; + + case ColorOrder::WBGR: + _idxW=0; _idxB=1; _idxG=2; _idxR=3; + break; + + case ColorOrder::RGBWC: + _idxR=0; _idxG=1; _idxB=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::GRBWC: + _idxG=0; _idxR=1; _idxB=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::BRGWC: + _idxB=0; _idxR=1; _idxG=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::RBGWC: + _idxR=0; _idxB=1; _idxG=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::GBRWC: + _idxG=0; _idxB=1; _idxR=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::BGRWC: + _idxB=0; _idxG=1; _idxR=2; _idxWW=3; _idxCW=4; + break; + + default: + _idxG=0; _idxR=1; _idxB=2; + break; } } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 3ace67be29..fd5c85efd3 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -262,6 +262,9 @@ class IBus { virtual void setBrightness(uint8_t b) { _brightness = b; } + + virtual uint32_t* getPixelData() { return _pixelData; } + virtual CctPixel* getCctData() { return _cctData; } }; //============================================================================== @@ -289,7 +292,7 @@ class LcdBusContext; */ class ColorEncoder { public: - ColorEncoder(ColorOrder order) : _order(order), _numChannels(getChannelCount(order)) {} + ColorEncoder(ColorOrder order); /** * Encode a single pixel to output buffer @@ -297,7 +300,7 @@ class ColorEncoder { * @param cct Optional CCT data (for CCT strips, replaces W channel) * @param out Output buffer (must have space for numChannels bytes) */ - void encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const; + inline void encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const; uint8_t getNumChannels() const { return _numChannels; } ColorOrder getOrder() const { return _order; } @@ -305,8 +308,31 @@ class ColorEncoder { private: ColorOrder _order; uint8_t _numChannels; + uint8_t _idxR; + uint8_t _idxG; + uint8_t _idxB; + uint8_t _idxW; + uint8_t _idxWW; + uint8_t _idxCW; }; +inline void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const { + uint8_t r = getR(pixel); + uint8_t g = getG(pixel); + uint8_t b = getB(pixel); + uint8_t w = getW(pixel); + + out[_idxR] = r; + out[_idxG] = g; + out[_idxB] = b; + if (_idxW != 0xFF) out[_idxW] = w; + + if (_idxWW != 0xFF) { + out[_idxWW] = cct ? cct->ww : w; // TODO: handle ww/cw more clever, there is not really a need to have this explicit, could save an if for RGB case + out[_idxCW] = cct ? cct->cw : 0; + } +} + //============================================================================== From b067e943008621154f846aa35f334994dd0f3f7c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 14 Mar 2026 09:47:49 +0100 Subject: [PATCH 039/173] added some todos, bugfixes, S2 I2S still not working --- wled00/FX_fcn.cpp | 5 +- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 6 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 74 +++++++++++++------- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 2 +- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index d0c87d98c7..3ec91a504a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1197,7 +1197,8 @@ void WS2812FX::finalizeInit() { // create buses/outputs unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers for (auto &bus : busConfigs) { - // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels + //TODO: this comment needs updating once the driver is finished + // 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 BusManager::allocateHardware(bus.type, bus.pins, bus.driverType); @@ -1213,7 +1214,7 @@ void WS2812FX::finalizeInit() { use_placeholder = true; } if (BusManager::add(bus, use_placeholder) != -1) { - mem += BusManager::busses.back()->getBusSize(); + mem += BusManager::busses.back()->getBusSize(); // TODO: check memory calculations if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count } } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 57d98427e9..013f9ce219 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -151,7 +151,7 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, Serial.printf("[WPB] createBus type=%u pin=%d bufSize=%u ch=%d\n", (unsigned)type, pin, bufferSize, channel); if (type == BusType::Auto) { - type = getRecommendedBusType(); + type = getRecommendedBusType(); // TODO: when is "auto" used? should auto default to RMT? it currently defaults to I2S Serial.printf("[WPB] Auto resolved to type=%u\n", (unsigned)type); } @@ -165,7 +165,11 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, #ifdef WLEDPB_I2S_SUPPORT case BusType::I2S: +#if defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32C3) + bus = new I2sBus(pin, timing, order, 0, bufferSize); +#else bus = new I2sBus(pin, timing, order, 1, bufferSize); +#endif break; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 7dbc237795..6169abcfda 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -74,7 +74,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { for (int i = 0; i < 2; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); if (!_dmaBuffer[i]) { - log_e("I2S DMA buffer alloc failed"); + Serial.println("I2S DMA buffer alloc failed"); deinit(); return false; } @@ -82,7 +82,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); if (!_dmaDesc[i]) { - log_e("I2S DMA desc alloc failed"); + Serial.println("I2S DMA desc alloc failed"); deinit(); return false; } @@ -155,7 +155,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // FIFO configuration _i2sDev->fifo_conf.val = 0; _i2sDev->fifo_conf.tx_fifo_mod_force_en = 1; - _i2sDev->fifo_conf.tx_fifo_mod = 1; // 16-bit single channel + _i2sDev->fifo_conf.tx_fifo_mod = 1; // 0=16bit dual, 1=16bit single, 2=32bit dual, 3=32bit single) _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold // PCM bypass @@ -165,12 +165,15 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // Channel config _i2sDev->conf_chan.val = 0; - _i2sDev->conf_chan.tx_chan_mod = 1; // Right channel + _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; @@ -185,7 +188,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { #if defined(WLEDPB_ESP32) const double baseClockMhz = 160.0; #else - const double baseClockMhz = 80.0; + const double baseClockMhz = 80.0; // S2 has 80MHz I2S base clock #endif // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz @@ -225,7 +228,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) _i2sDev->sample_rate_conf.val = 0; _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; - _i2sDev->sample_rate_conf.tx_bits_mod = 8; + _i2sDev->sample_rate_conf.tx_bits_mod = 8; // TODO: try 16 bit mode an bump up outputs to 16 parallel channels, just like LCD driver does now // Final reset before ISR install _i2sDev->lc_conf.in_rst = 1; _i2sDev->lc_conf.out_rst = 1; @@ -247,7 +250,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, dmaISR, this, &_isrHandle); if (err != ESP_OK) { - log_e("I2S ISR alloc failed: %d", err); + Serial.printf("I2S ISR alloc failed: %d", err); deinit(); return false; } @@ -316,7 +319,7 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { #if defined(WLEDPB_ESP32) sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; #elif defined(WLEDPB_ESP32S2) - sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes + sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX #else sigIdx = I2S0O_DATA_OUT0_IDX; #endif @@ -452,6 +455,12 @@ void I2sBusContext::fillBuffer(uint8_t bufIdx) { // desc->length stays at _bufferSize (set in init, never changes) } +// ----- I2S ISR Tracking ----- +static volatile uint32_t s_i2sIsrCount = 0; +static volatile uint32_t s_i2sIsrSending = 0; +static volatile uint32_t s_i2sIsrReset = 0; +static volatile uint32_t s_i2sIsrIdle = 0; + bool I2sBusContext::startTransmit() { if (_state != DriverState::Idle) return false; if (_channelCount == 0) return false; @@ -482,12 +491,18 @@ bool I2sBusContext::startTransmit() { _state = DriverState::Sending; // Reset DMA and FIFO before starting - _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->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; // Clear and enable interrupts _i2sDev->int_clr.val = 0xFFFFFFFF; @@ -500,14 +515,26 @@ bool I2sBusContext::startTransmit() { _i2sDev->out_link.start = 1; _i2sDev->conf.tx_start = 1; + // ----- DEBUG BLOCK START ----- + /* + static uint32_t last_isr = 0; + uint32_t diff_isr = s_i2sIsrCount - last_isr; + last_isr = s_i2sIsrCount; + Serial.printf("[I2S-Tx] startTransmit triggering. ISR count delta since last tx: %u\n", diff_isr); + Serial.printf("[I2S-Tx] State vars: isrTotal=%u, send=%u, reset=%u, idle=%u\n", + s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle); + Serial.printf("[I2S-Tx] HW Regs: conf(0x%08x) conf1(0x%08x) conf2(0x%08x)\n", + _i2sDev->conf.val, _i2sDev->conf1.val, _i2sDev->conf2.val); + Serial.printf("[I2S-Tx] int_ena(0x%08x) int_raw(0x%08x)\n", + _i2sDev->int_ena.val, _i2sDev->int_raw.val); + Serial.printf("[I2S-Tx] out_link(0x%08x) lc_conf(0x%08x)\n", + _i2sDev->out_link.val, _i2sDev->lc_conf.val); + */ + // ----- DEBUG BLOCK END ----- + return true; } -static volatile uint32_t s_i2sIsrCount = 0; -static volatile uint32_t s_i2sIsrSending = 0; -static volatile uint32_t s_i2sIsrReset = 0; -static volatile uint32_t s_i2sIsrIdle = 0; - void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { I2sBusContext* ctx = (I2sBusContext*)arg; i2s_dev_t* dev = ctx->_i2sDev; @@ -517,14 +544,14 @@ void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { if (!(status & I2S_OUT_EOF_INT_ST)) return; - s_i2sIsrCount++; + s_i2sIsrCount++; // debug, remove // The completed buffer just finished playing; DMA is now on the other buffer uint8_t completedBuf = ctx->_activeBuffer; ctx->_activeBuffer ^= 1; if (ctx->_state == DriverState::Sending) { - s_i2sIsrSending++; + s_i2sIsrSending++; // debug, remove // Encode next chunk into the completed buffer // encode4Step always fills the full buffer (zeros for any remainder) ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); @@ -552,11 +579,11 @@ void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { // Fill completed buffer with zeros (reset signal) so it plays after. memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); ctx->_dmaDesc[completedBuf]->owner = 1; - s_i2sIsrReset++; + s_i2sIsrReset++; // debug, remove ctx->_state = DriverState::WaitingReset; } else { // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. - s_i2sIsrIdle++; + s_i2sIsrIdle++; // debug, remove dev->int_ena.out_eof = 0; dev->conf.tx_start = 0; dev->out_link.start = 0; @@ -691,6 +718,5 @@ void I2sBus::setColorOrder(ColorOrder order) { } - } // namespace WLEDpixelBus #endif // WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index 355006a9e1..d627e65385 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -15,7 +15,7 @@ namespace WLEDpixelBus { #include "esp_intr_alloc.h" #if defined(WLEDPB_ESP32) - #define WLEDPB_I2S_BUS_COUNT 2 + #define WLEDPB_I2S_BUS_COUNT 2 // TODO: support both buses on ESP32? (currently only bus 1 is used for LED output, one is for AR) #define WLEDPB_I2S_MAX_CHANNELS 16 #else #define WLEDPB_I2S_BUS_COUNT 1 From a7501bec40c8c46daeca2e66db853ad40a8475d7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 14 Mar 2026 12:34:34 +0100 Subject: [PATCH 040/173] some fixes for ESP8266, only bitbanging works reliably (UART partially works, DMA is glitchy) --- wled00/bus_manager.cpp | 11 +++++--- wled00/bus_manager.h | 2 +- wled00/bus_wrapper.h | 21 +++++++++----- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 38 +++++++++++++++++++------- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index bbeda5430b..42893124d9 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -1262,11 +1262,15 @@ String BusManager::getLEDTypesJSONString() { bool BusManager::allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { return PolyBus::allocateHardware(busType, pins, driverType); } +// Module-level cache for setPixelColor() fast path - must be reset in removeAll() +static Bus* _lastBusCache = nullptr; + //do not call this method from system context (network callback) void BusManager::removeAll() { DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); + _lastBusCache = nullptr; // Reset cache before destroying buses to avoid dangling pointer UB busses.clear(); #ifndef ESP8266 // Reset channel tracking for fresh allocation @@ -1353,15 +1357,14 @@ void BusManager::show() { } void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { - static Bus* lastBus = nullptr; - if (lastBus && lastBus->containsPixel(pix)) { - lastBus->setPixelColor(pix - lastBus->getStart(), c); + if (_lastBusCache && _lastBusCache->containsPixel(pix)) { + _lastBusCache->setPixelColor(pix - _lastBusCache->getStart(), c); return; } for (auto &bus : busses) { if (bus->containsPixel(pix)) { bus->setPixelColor(pix - bus->getStart(), c); - lastBus = bus.get(); + _lastBusCache = bus.get(); return; } } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 5ae5d6edc8..da96dacd99 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -276,7 +276,7 @@ class BusDigital : public Bus { private: uint8_t _skip; uint8_t _colorOrder; - uint8_t _pins[2]; + uint8_t _pins[2] = {255, 255}; uint8_t _driverType; // 0=RMT (default), 1=I2S uint16_t _frequencykHz; uint16_t _milliAmpsMax; diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 425ea2194e..8027c0879c 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -199,13 +199,13 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } auto btype = WLEDpixelBus::BusType::Auto; - uint8_t hwIndex = pins[0]; #ifdef ESP8266 - uint8_t offset = pins[0] - 1; - if (offset == 0 || offset == 1) btype = WLEDpixelBus::BusType::UART; - else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; - else btype = WLEDpixelBus::BusType::BitBang; + // uint8_t offset = pins[0] - 1; + // if (pins[0] == 1 || pins[0] == 2) btype = WLEDpixelBus::BusType::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled TODO: there is a bug, TX1 only works after saving, not after reboot, needs investigation, use bitbanging for now + //else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; // DMA method uses a lot of RAM! + //else btype = WLEDpixelBus::BusType::BitBang; + btype = WLEDpixelBus::BusType::BitBang; // bit banging works on all pins and uses less memory #else switch (driverType) { case 0: btype = WLEDpixelBus::BusType::RMT; break; @@ -236,7 +236,7 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #endif - return WLEDpixelBus::createBus(btype, hwIndex, proto.timing, finalOrder, WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, rmtCh); + return WLEDpixelBus::createBus(btype, pins[0], proto.timing, finalOrder, WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, rmtCh); } static unsigned memUsage(uint8_t busType, unsigned count, const uint8_t* pins, unsigned driverType = 0) { @@ -258,7 +258,14 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, auto btype = WLEDpixelBus::BusType::Auto; #ifdef ESP8266 - btype = WLEDpixelBus::BusType::Auto; + // Determine the actual bus type from pin number (must mirror the logic in create()) + // Pin 1 → UART0, Pin 2 → UART1, Pin 3 → DMA (I2S), all others → BitBang + //uint8_t offset = (pins && pins[0] >= 1) ? (pins[0] - 1) : 255; + //if (pins[0] == 1 || pins[0] == 2) btype = WLEDpixelBus::BusType::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled + //else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; -> DMA method uses too much RAM (it is also not working right currently) + //else btype = WLEDpixelBus::BusType::BitBang; + + btype = WLEDpixelBus::BusType::BitBang; #else switch (driverType) { case 0: btype = WLEDpixelBus::BusType::RMT; break; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index fd5c85efd3..a7398fcfe8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -394,16 +394,34 @@ static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t ch // Bus instance overhead mem += 128; // Approximate C++ object size + IBus data structures - - // Encode buffer (allocated continuously across RMT, I2S, LCD) - mem += numPixels * channelCount; - - // DMA buffers and descriptors for I2S/LCD - // They are double-buffered and mapped via heap_caps_malloc - if (type == BusType::I2S || type == BusType::LCD || type == BusType::Auto) { - mem += dmaBufferSize * 2; - // Include minimal descriptor memory estimates (~64 bytes per context) - mem += 64; + // TODO: check if this is correct + switch (type) { + case BusType::UART: + // UART encode buffer: 4 encoded bytes per LED byte (no DMA) + mem += numPixels * channelCount * 4; + break; + + case BusType::BitBang: + mem += numPixels * channelCount; // single buffer + break; + + case BusType::DMA: + // I2S DMA on ESP8266: same 4-step encoding as UART but written via DMA + mem += numPixels * channelCount * 4; + break; + + default: + // Encode buffer (RMT, I2S, LCD, SPI, etc.) + mem += numPixels * channelCount; + + // DMA buffers and descriptors for I2S/LCD + // They are double-buffered and mapped via heap_caps_malloc + if (type == BusType::I2S || type == BusType::LCD || type == BusType::Auto) { + mem += dmaBufferSize * 2; + // Include minimal descriptor memory estimates (~64 bytes per context) + mem += 64; + } + break; } return mem; From 92a89e4d436d617ead643ab1ce44eed67709e0df Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 14 Mar 2026 15:41:39 +0100 Subject: [PATCH 041/173] some optimizations (lots more to leverage), added todo's C3 parallel SPI output is currently broken, when adding a second output, first output starts glitching --- wled00/bus_manager.cpp | 42 +++++++++++-------- wled00/bus_wrapper.h | 6 +-- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 11 +++-- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 25 ++++++----- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 13 +++--- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 16 +++---- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 4 +- 7 files changed, 63 insertions(+), 54 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 42893124d9..da0c387bfc 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -162,15 +162,14 @@ BusDigital::BusDigital(const BusConfig &bc) // fix for wled#4759 if (_valid) { - _busPtr->allocatePixelBuffer(lenToCreate + _skip, _hasCCT); - _pixelDataPtr = _busPtr->getPixelData(); + _valid = _busPtr->allocatePixelBuffer(lenToCreate + _skip, _hasCCT); // returns false if allocation fails + _pixelDataPtr = _busPtr->getPixelData(); // can be null if allocation failed _cctDataPtr = _busPtr->getCctData(); - for (unsigned i = 0; i < _skip; i++) { - if (_pixelDataPtr) _pixelDataPtr[i] = 0; // set sacrificial pixels to black - } - } else { - cleanup(); } + + if (!_valid) + cleanup(); + 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, @@ -272,19 +271,29 @@ bool BusDigital::canShow() const { //TODO only show if no new show due in the next 50ms void BusDigital::setStatusPixel(uint32_t c) { if (_valid && _skip) { - _busPtr->setPixelColor(0, c); + _pixelDataPtr[0] = c; if (canShow()) _busPtr->show(); } } // note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash +// TODO: this function needs some optimization, making better use of the new bus architecture void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; + if (_reversed) pix = _len - pix -1; + pix += _skip; + 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 (hasWhite()) { + c = autoWhiteCalc(c, cctWW, cctCW); + // if (hasCCT()) { + // _cctDataPtr[pix].ww = cctWW; // TODO: uncomment after brightness scaling has been moved to bus level. + // _cctDataPtr[pix].cw = cctCW; + // } + } + c = color_fade(c, _bri, true); // apply brightness TODO: move this to bus level? requires the ABL to also be on bus level (which for per bus ABL makes sense) and we can do some trickery: sum up unscaled pixels brightness, then apply the factor for global ABL. if (hasCCT()) { wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte) @@ -302,10 +311,8 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { } } - 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 + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs TODO: move this to bus level? would make sense to have a bus type for this. unsigned pOld = pix; pix = IC_INDEX_WS2812_1CH_3X(pix); uint32_t cOld = _busPtr->getPixelColor(pix); @@ -316,13 +323,12 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { } } - if (hasCCT() && _cctDataPtr) { - _cctDataPtr[pix].ww = wwcw & 0xFF; + // set pixels directly, note: if pointers are invalid, _valid is set to false in alloc function. TODO: need to check for out of bounds or is this safe by design? + if (hasCCT()) { + _cctDataPtr[pix].ww = wwcw & 0xFF; // TODO: check hasCCT() above and directly set this after autowhitecalc. _cctDataPtr[pix].cw = wwcw >> 8; } - if (_pixelDataPtr) { - _pixelDataPtr[pix] = c; - } + _pixelDataPtr[pix] = c; } // returns lossly restored color from bus diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 8027c0879c..c7bdb707d8 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -91,9 +91,9 @@ class PolyBus { if (busPtr) return static_cast(busPtr)->canShow(); return true; } - static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint32_t cW) { - if (busPtr) static_cast(busPtr)->setPixelColor(pix, c); - } +// static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint32_t cW) { +// if (busPtr) static_cast(busPtr)->setPixelColor(pix, c); +// } static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { if (busPtr) static_cast(busPtr)->setBrightness(b); } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 013f9ce219..05e076e229 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -204,17 +204,20 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, return bus; } +// TODO: this function is not really needed, bus type defaults should be handled by busmanager or buswrapper BusType getRecommendedBusType() { +/* #if defined(WLEDPB_ESP32S3) - return BusType::LCD; // S3 has best LCD support + return BusType::LCD; // S3 has LCD support #elif defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) return BusType::I2S; // Original and S2 have I2S parallel #elif defined(WLEDPB_SPI_SUPPORT) return BusType::SPI; // C3 uses SPI quad mode -#elif defined(WLEDPB_ESP8266) - return BusType::UART; + */ +#if defined(WLEDPB_ESP8266) + return BusType::BitBang; // use bitbanging by default (most versatile) #else - return BusType::RMT; // Fallback to RMT + return BusType::RMT; // use RMT by default (most versatile) #endif } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index a7398fcfe8..0f1ca25265 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -228,30 +228,35 @@ class IBus { free(_cctData); _cctData = nullptr; } - _numPixels = numPixels; if (numPixels == 0) return true; - + // TODO: use WLED alloc functions that are min-heap safe _pixelData = (uint32_t*)malloc(numPixels * sizeof(uint32_t)); if (!_pixelData) return false; memset(_pixelData, 0, numPixels * sizeof(uint32_t)); - + if (hasCCT) { _cctData = (CctPixel*)malloc(numPixels * sizeof(CctPixel)); + if (!_cctData) { + free(_pixelData); + _pixelData = nullptr; + return false; + } if (_cctData) memset(_cctData, 0, numPixels * sizeof(CctPixel)); } - + return true; } - virtual void setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) { - if (pix >= _numPixels || !_pixelData) return; - _pixelData[pix] = c; - if (cp && _cctData) _cctData[pix] = *cp; - } + // TODO: can be removed, pixels are now written to the buffer directly in busDigital::setPixelColor() + // virtual void IRAM_ATTR setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) { + // if (pix >= _numPixels) return; // TODO: can this be removed safely? busmanager already checks? its in the hot path. + // _pixelData[pix] = c; + // if (cp && _cctData) _cctData[pix] = *cp; // TODO: make set cct a seperate function? might speed things up by removing this check + // } virtual uint32_t getPixelColor(uint16_t pix) const { - if (pix >= _numPixels || !_pixelData) return 0; + if (pix >= _numPixels) return 0; return _pixelData[pix]; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index b932a3eae4..369d907f1a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -32,7 +32,7 @@ Esp8266UartBus::~Esp8266UartBus() { } void Esp8266UartBus::buildLut() { - if (!_encodeLut) _encodeLut = (uint8_t*)malloc(256 * 4); + if (!_encodeLut) _encodeLut = (uint8_t*)malloc(256 * 4); // TODO: this is really bad memory management! should only have a pixel buffer and do encoding on the fly if (!_encodeLut) return; const uint8_t uartData[4] = {0b110111, 0b000111, 0b110100, 0b000100}; for (int i=0; i<256; i++) { @@ -86,13 +86,10 @@ void Esp8266UartBus::updateUartTiming() { USC0(uartNum) &= ~(fifoResetFlags); // clear all invert bits - USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI)); + USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI) | (1 << UCTXI)); - // set inverted logic for TX - USC0(uartNum) |= (1 << UCTXI); - - // Disable RX & TX interrupts that might have been enabled by Arduino Core Serial - USIE(uartNum) &= ~((1 << UIFF) | (1 << UIFE)); + // Disable RX & TX interrupts that might have been enabled by Arduino Core Serial -> needed? + //USIE(uartNum) &= ~((1 << UIFF) | (1 << UIFE)); } bool Esp8266UartBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { @@ -100,7 +97,7 @@ bool Esp8266UartBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP if (!_initialized || !pixels || numPixels == 0) return false; size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); - size_t outLen = numPixels * bpp * 4; + size_t outLen = numPixels * bpp * 4; // TODO: should not be *4, we can encode on the fly if done right. if (!allocateBuffer(outLen)) return false; ColorEncoder encoder(_order); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 664f38f3fe..255ad74687 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -35,8 +35,8 @@ static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_O // Encoding patterns for SPI quad mode (4-step cadence, LSB first) // Each lane is one bit position in a nibble, one byte = two clock cycles, 2 bytes = one 4-step bit -static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high -static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high +static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high (0000 0000 0000 0001 in binary, output LSB first) +static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high (0000 0001 0001 0001 in binary, output LSB first) SpiBusContext* SpiBusContext::_instance = nullptr; uint8_t SpiBusContext::_refCount = 0; @@ -86,7 +86,7 @@ SpiBusContext::~SpiBusContext() { bool SpiBusContext::isSpiDone() { if (!_hasStarted) return true; // no transfer ever started - // We are logically done once all real data is encoded + // We are logically done once all real data is encoded TODO: should add a timeout here for more accurate end of frame timing if (!_sending) { return true; } @@ -416,7 +416,7 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ // Mark this channel as staged _stagedMask |= (1 << channelIdx); - if (len > _numBytes) _numBytes = len; + if (len > _numBytes) _numBytes = len; // update longest bus length } void SpiBusContext::resetAndStart() { @@ -471,11 +471,14 @@ bool SpiBusContext::startTransmit() { spi_ll_user_start(_hw); } else { // Hardware already running - just update pointers, ISR picks up new data + // note: stopping the SPI between frames had some serious issues of the driver stalling all the time, that may be fixable but documentation is scarce + // leaving the SPI running continuously and feeding in new data if available seems to be working more reliably gdma_dev_t* dma = &GDMA; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; _framePos = 0; _numBytes = newBytes; _sending = true; + // TODO: do we need to validate that the correct buffer will be written when the interrupt is re-enabled? dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; } @@ -564,6 +567,7 @@ bool ParallelSpiBus::allocateBuffer(uint16_t numPixels) { return true; } +// TODO: this actually only uses internal buffer, could get rid of the parameters that are now unused. bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; @@ -618,10 +622,6 @@ void ParallelSpiBus::setColorOrder(ColorOrder order) { _order = order; } -void ParallelSpiBus::setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp) { - IBus::setPixelColor(pix, c, cp); -} - } // namespace WLEDpixelBus #endif // WLEDPB_SPI_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index b32f7c2dd4..7ad87ef7fc 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -10,7 +10,7 @@ namespace WLEDpixelBus { #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines -#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 +#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 // must be a multiple of 16 (16 DMA bytes per source byte) #define WLEDPB_SPI_GDMA_CHANNEL 0 class ParallelSpiBus; @@ -100,8 +100,6 @@ class ParallelSpiBus : public IBus { void waitComplete() override; const char* getType() const override { return "SPI"; } - void setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) override; - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(ColorOrder order); From 2d1fe694cbd0374022f938686810b194b521bce1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 15 Mar 2026 10:56:10 +0100 Subject: [PATCH 042/173] rework of C3 SPI driver, still can cause deadlocks, stalls and crashes. --- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 266 ++++++++++-------- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 3 +- 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 255ad74687..fb951c5a30 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -9,6 +9,8 @@ #include "esp_private/gdma.h" #include "esp_rom_gpio.h" +volatile bool txdone = false; // TODO: integrate this into driver, i.e. non global +volatile bool isSending = false; // TODO: integrate this into driver, i.e. non global namespace WLEDpixelBus { //============================================================================== @@ -20,7 +22,7 @@ namespace WLEDpixelBus { static inline void spi_ll_apply_config(spi_dev_t *hw) { hw->cmd.update = 1; - while (hw->cmd.update); //waiting config applied + //while (hw->cmd.update); //waiting config applied } static inline void spi_ll_user_start(spi_dev_t *hw) @@ -85,14 +87,15 @@ SpiBusContext::~SpiBusContext() { bool SpiBusContext::isSpiDone() { if (!_hasStarted) return true; // no transfer ever started - + //return txdone; // We are logically done once all real data is encoded TODO: should add a timeout here for more accurate end of frame timing if (!_sending) { - return true; + //delay(10); // wait for finish sending !!! this is a hacky test, to check if this locks stuff up. remove again -> seems to fix the glitching and stalls, need a better solution though + return txdone; } - // Fail-safe watchdog: if stuck for > 250ms, recover it - if (millis() - _lastTransmitMs > 250) { + // Fail-safe watchdog: if stuck for > 500ms, recover it + if (millis() - _lastTransmitMs > 50) { forceIdle(); return true; } @@ -102,16 +105,20 @@ bool SpiBusContext::isSpiDone() { void SpiBusContext::forceIdle() { _sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global _stagedMask = 0; + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + // Stop DMA immediately gdma_dev_t* dma = &GDMA; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); if (_hw) { - _hw->cmd.usr = 0; - _hw->dma_int_clr.val = 0xFFFFFFFF; + _hw->cmd.usr = 0; // stop SPI user transfer (will output a fast clock, so detach pins from spi before) + _hw->dma_int_clr.val = 0xFFFFFFFF; // } } @@ -129,6 +136,7 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { if (srcThisChunk == 0) { _sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global _framePos = 0; return; } @@ -166,63 +174,65 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { // (FIFO underrun recovery). outfifo_empty_err is temporarily disabled after // handling to prevent infinite ISR loops; trans_done re-enables it. void IRAM_ATTR SpiBusContext::spiISR(void* arg) { - SpiBusContext* ctx = (SpiBusContext*)arg; - uint32_t raw = ctx->_hw->dma_int_raw.val; - - if (raw & 0x02) { - // outfifo_empty_err: FIFO starved, disable to prevent ISR loop - ctx->_hw->dma_int_ena.outfifo_empty_err = 0; - ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags - ctx->_hw->cmd.usr = 0; // Stop SPI - - spi_ll_dma_tx_fifo_reset(ctx->_hw); - spi_ll_outfifo_empty_clr(ctx->_hw); - - gdma_dev_t* dma = &GDMA; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - ctx->_dmaDesc[0].owner = 1; - ctx->_dmaDesc[1].owner = 1; - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&ctx->_dmaDesc[ctx->_currentBuffer]); - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - - spi_ll_clear_int_stat(ctx->_hw); - spi_ll_set_mosi_bitlen(ctx->_hw, 253952); - ctx->_hw->cmd.usr = 1; - return; + SpiBusContext* ctx = (SpiBusContext*)arg; + uint32_t raw = ctx->_hw->dma_int_raw.val; + ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags + + if (raw & SPI_TRANS_DONE_INT_ST) { + txdone = true; // transfer finished + // Serial.print("b"); + } + else { + // Serial.println(raw, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST + // this may be some SPI error, stop SPI and DMA to prevent lockup + // detach pins from spi and force low to prevent any output when cmd.user is stopped (which outputs a fast pattern) + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { + gpio_matrix_out(ctx->_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); + gpio_set_level((gpio_num_t)ctx->_channels[i].pin, 0); + } } + //ctx->_hw->cmd.usr = 0; // Stop SPI -> this cauuses a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) + //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks + ctx->forceIdle(); + txdone = true; // still mark it as done + } + + +//digitalWrite(0, HIGH); //!!! note: setting pins inside ISRs can cause crashes - if (raw & 0x1000) { - ctx->_hw->dma_int_clr.trans_done = 1; - spi_ll_set_mosi_bitlen(ctx->_hw, 253952); - ctx->_hw->cmd.usr = 1; - ctx->_hw->dma_int_ena.outfifo_empty_err = 1; - } } void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; gdma_dev_t* dma = &GDMA; + // Serial.print("*"); + // toggle gpio0 + // digitalWrite(0, HIGH); if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { uint8_t completedBuf = ctx->_currentBuffer; ctx->encodeSpiChunk(completedBuf); + // if (isSending) { // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI - ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; - ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; - ctx->_dmaDesc[completedBuf].eof = 1; - ctx->_dmaDesc[completedBuf].owner = 1; - ctx->_currentBuffer = completedBuf ? 0 : 1; - } - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.val; + // ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + // ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + //ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? + //ctx->_dmaDesc[completedBuf].owner = 1; + // } + ctx->_currentBuffer = completedBuf ? 0 : 1; // toggle DMA buffer + } +// digitalWrite(0, LOW); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel } bool SpiBusContext::init(const LedTiming& timing) { if (_initialized) return true; + //pinMode(0, OUTPUT); + //digitalWrite(0, LOW); //!!! + // Allocate DMA buffers for (int i = 0; i < 2; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, @@ -260,6 +270,7 @@ bool SpiBusContext::init(const LedTiming& timing) { 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; @@ -287,10 +298,10 @@ bool SpiBusContext::init(const LedTiming& timing) { spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); - // Route SPI clock to a dummy pin (needed for DMA to work) - pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + // 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); + // 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); @@ -303,7 +314,7 @@ bool SpiBusContext::init(const LedTiming& timing) { gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + // gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; @@ -336,6 +347,7 @@ bool SpiBusContext::init(const LedTiming& timing) { void SpiBusContext::deinit() { _sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global // Stop SPI and DMA before freeing resources if (_hw) { @@ -409,80 +421,82 @@ void SpiBusContext::unregisterChannel(int8_t channelIdx) { } void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - - // Mark this channel as staged - _stagedMask |= (1 << channelIdx); - - if (len > _numBytes) _numBytes = len; // update longest bus length -} + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; -void SpiBusContext::resetAndStart() { - spi_ll_dma_tx_fifo_reset(_hw); - spi_ll_outfifo_empty_clr(_hw); - spi_ll_apply_config(_hw); + // Mark this channel as staged + _stagedMask |= (1 << channelIdx); - _dmaDesc[0].owner = 1; - _dmaDesc[1].owner = 1; - - gdma_dev_t* dma = &GDMA; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); - gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + if (len > _numBytes) _numBytes = len; // update longest bus length } -bool SpiBusContext::startTransmit() { - if (_sending) return false; - if (_channelCount == 0) return false; - - if (_stagedMask != _channelMask) return false; - _stagedMask = 0; - _lastTransmitMs = millis(); - size_t newBytes = 0; - for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcLen > newBytes) { - newBytes = _channels[ch].srcLen; - } - } - - if (!_hasStarted) { - _framePos = 0; - _numBytes = newBytes; - - // Max buffer-aligned bit count: continuous output for ~97ms before - // trans_done restarts. outfifo_empty_err handles FIFO underruns. - spi_ll_set_mosi_bitlen(_hw, 253952); - spi_ll_clear_int_stat(_hw); - - _sending = true; - _currentBuffer = 0; - encodeSpiChunk(0); - encodeSpiChunk(1); - - resetAndStart(); - _hasStarted = true; - spi_ll_user_start(_hw); - } else { - // Hardware already running - just update pointers, ISR picks up new data - // note: stopping the SPI between frames had some serious issues of the driver stalling all the time, that may be fixable but documentation is scarce - // leaving the SPI running continuously and feeding in new data if available seems to be working more reliably - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - _framePos = 0; - _numBytes = newBytes; - _sending = true; - // TODO: do we need to validate that the correct buffer will be written when the interrupt is re-enabled? - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - } - - return true; +bool SpiBusContext::startTransmit() { + if (_sending) return false; + if (_channelCount == 0) return false; + + if (_stagedMask != _channelMask) return false; // not all channels ready yet + _stagedMask = 0; + + _lastTransmitMs = millis(); + size_t newBytes = 0; + for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { + if (_channels[ch].active && _channels[ch].srcLen > newBytes) { + newBytes = _channels[ch].srcLen; + } + } + + _framePos = 0; + _numBytes = newBytes; + txdone = false;//!!! + + // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte + // Plus reset: extra zero bits at the end + uint32_t resetBits = 1024;//5000; // ~300us reset at ~2.6MHz TODO: calculate from actual reset pulse, make sure its 4 byte aligned (just in case) + uint32_t totalBits = (_numBytes * 16 * 8) + resetBits; + if (totalBits > 262143) totalBits = 262143; // SPI max 18 bits for length register + + spi_ll_set_mosi_bitlen(_hw, totalBits); + spi_ll_clear_int_stat(_hw); + + _sending = true; + isSending = true; // TODO: integrate this into driver, i.e. non global + _currentBuffer = 0; + encodeSpiChunk(0); + encodeSpiChunk(1); + + _dmaDesc[0].owner = 1; // make sure the DMA owns the buffer + _dmaDesc[1].owner = 1; + //digitalWrite(0, LOW);//!!! + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_dma_tx_enable(_hw, true); + spi_ll_apply_config(_hw); + + // re-attach pins to SPI signals + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0) { + pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], false, false); // TODO: should this trick also be used to force outputs low during reset pulse? + } + } + + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); + delayMicroseconds(50); // Short delay to ensure DMA has started before SPI: if DMA did not start, SPI will immediately stall with "outfifo_empty_err", short delay makes it less prone (this may be yet another IDF V4 bug) + // Serial.print("a"); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + //dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + _hasStarted = true; + spi_ll_user_start(_hw); // start SPI user transfer + // TODO: there are still some glitches/stalls due to tx_fifo underruns but it is unclear why, it may even be an IDF V4 bug (I read about some timing issue with buffer handover or a missing nop) + // even when not accessing the UI there seems to be some dead-lock halting the system until the WDT kicks... + + + return true; } // SpiBus implementation @@ -533,7 +547,7 @@ void ParallelSpiBus::end() { if (_ctx) { uint32_t startWait = millis(); while (!_ctx->isIdle()) { - if (millis() - startWait > 500) { + if (millis() - startWait > 100) { break; } vTaskDelay(1); @@ -572,6 +586,7 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; // Wait for previous transmission + /* uint32_t startWait = millis(); while (!_ctx->isIdle()) { if (millis() - startWait > 500) { @@ -579,9 +594,10 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP return false; } vTaskDelay(1); - } + }*/ // Wait for SPI to finish any remaining transfer + /* startWait = millis(); while (!_ctx->isSpiDone()) { if (millis() - startWait > 500) { @@ -589,7 +605,7 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP return false; } vTaskDelay(1); - } + }*/ if (!allocateBuffer(_numPixels)) return false; @@ -609,13 +625,13 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP bool ParallelSpiBus::canShow() const { if (!_ctx) return true; - return _ctx->isIdle() && _ctx->isSpiDone(); + return _ctx->isSpiDone(); } void ParallelSpiBus::waitComplete() { - while (_ctx && !_ctx->isIdle()) { + // while (_ctx && !_ctx->isIdle()) { vTaskDelay(1); - } + // } } void ParallelSpiBus::setColorOrder(ColorOrder order) { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index 7ad87ef7fc..79f1ebe3a9 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -10,7 +10,7 @@ namespace WLEDpixelBus { #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines -#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 // must be a multiple of 16 (16 DMA bytes per source byte) +#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (1k per buffer means about 0.5ms interrupt intervals) #define WLEDPB_SPI_GDMA_CHANNEL 0 class ParallelSpiBus; @@ -43,7 +43,6 @@ class SpiBusContext { ~SpiBusContext(); void encodeSpiChunk(uint8_t bufIdx); - void resetAndStart(); static void IRAM_ATTR gdmaISR(void* arg); static void IRAM_ATTR spiISR(void* arg); From 42f98137061ea125ae7e1fa9a1cb86fe6ce95160 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 15 Mar 2026 15:31:33 +0100 Subject: [PATCH 043/173] reformat to wled default indentation --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 288 ++--- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 404 +++--- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 432 +++---- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 114 +- .../src/WLEDpixelBus/WLEDpixelBus_Features.h | 86 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 1128 ++++++++--------- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 192 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 872 ++++++------- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 172 +-- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 772 +++++------ .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 146 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 504 ++++---- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 106 +- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 158 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h | 62 +- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 96 +- 16 files changed, 2766 insertions(+), 2766 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 05e076e229..5b5b785a2f 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -35,110 +35,110 @@ namespace WLEDpixelBus { ColorEncoder::ColorEncoder(ColorOrder order) : _order(order), _numChannels(getChannelCount(order)) { - _idxR = _idxG = _idxB = _idxW = _idxWW = _idxCW = 0xFF; + _idxR = _idxG = _idxB = _idxW = _idxWW = _idxCW = 0xFF; //TODO: is there a better way to define color orders indices than just this dumb list? it also does not cover all possibilities I think (swapping white and color channels) - switch(order) - { - case ColorOrder::RGB: - _idxR=0; _idxG=1; _idxB=2; - break; - - case ColorOrder::GRB: - _idxG=0; _idxR=1; _idxB=2; - break; - - case ColorOrder::BRG: - _idxB=0; _idxR=1; _idxG=2; - break; - - case ColorOrder::RBG: - _idxR=0; _idxB=1; _idxG=2; - break; - - case ColorOrder::GBR: - _idxG=0; _idxB=1; _idxR=2; - break; - - case ColorOrder::BGR: - _idxB=0; _idxG=1; _idxR=2; - break; - - case ColorOrder::RGBW: - _idxR=0; _idxG=1; _idxB=2; _idxW=3; - break; - - case ColorOrder::GRBW: - _idxG=0; _idxR=1; _idxB=2; _idxW=3; - break; - - case ColorOrder::BRGW: - _idxB=0; _idxR=1; _idxG=2; _idxW=3; - break; - - case ColorOrder::RBGW: - _idxR=0; _idxB=1; _idxG=2; _idxW=3; - break; - - case ColorOrder::GBRW: - _idxG=0; _idxB=1; _idxR=2; _idxW=3; - break; - - case ColorOrder::BGRW: - _idxB=0; _idxG=1; _idxR=2; _idxW=3; - break; - - case ColorOrder::WRGB: - _idxW=0; _idxR=1; _idxG=2; _idxB=3; - break; - - case ColorOrder::WGRB: - _idxW=0; _idxG=1; _idxR=2; _idxB=3; - break; - - case ColorOrder::WBRG: - _idxW=0; _idxB=1; _idxR=2; _idxG=3; - break; - - case ColorOrder::WRBG: - _idxW=0; _idxR=1; _idxB=2; _idxG=3; - break; - - case ColorOrder::WGBR: - _idxW=0; _idxG=1; _idxB=2; _idxR=3; - break; - - case ColorOrder::WBGR: - _idxW=0; _idxB=1; _idxG=2; _idxR=3; - break; - - case ColorOrder::RGBWC: - _idxR=0; _idxG=1; _idxB=2; _idxWW=3; _idxCW=4; - break; - - case ColorOrder::GRBWC: - _idxG=0; _idxR=1; _idxB=2; _idxWW=3; _idxCW=4; - break; - - case ColorOrder::BRGWC: - _idxB=0; _idxR=1; _idxG=2; _idxWW=3; _idxCW=4; - break; - - case ColorOrder::RBGWC: - _idxR=0; _idxB=1; _idxG=2; _idxWW=3; _idxCW=4; - break; - - case ColorOrder::GBRWC: - _idxG=0; _idxB=1; _idxR=2; _idxWW=3; _idxCW=4; - break; - - case ColorOrder::BGRWC: - _idxB=0; _idxG=1; _idxR=2; _idxWW=3; _idxCW=4; - break; - - default: - _idxG=0; _idxR=1; _idxB=2; - break; - } + switch(order) + { + case ColorOrder::RGB: + _idxR=0; _idxG=1; _idxB=2; + break; + + case ColorOrder::GRB: + _idxG=0; _idxR=1; _idxB=2; + break; + + case ColorOrder::BRG: + _idxB=0; _idxR=1; _idxG=2; + break; + + case ColorOrder::RBG: + _idxR=0; _idxB=1; _idxG=2; + break; + + case ColorOrder::GBR: + _idxG=0; _idxB=1; _idxR=2; + break; + + case ColorOrder::BGR: + _idxB=0; _idxG=1; _idxR=2; + break; + + case ColorOrder::RGBW: + _idxR=0; _idxG=1; _idxB=2; _idxW=3; + break; + + case ColorOrder::GRBW: + _idxG=0; _idxR=1; _idxB=2; _idxW=3; + break; + + case ColorOrder::BRGW: + _idxB=0; _idxR=1; _idxG=2; _idxW=3; + break; + + case ColorOrder::RBGW: + _idxR=0; _idxB=1; _idxG=2; _idxW=3; + break; + + case ColorOrder::GBRW: + _idxG=0; _idxB=1; _idxR=2; _idxW=3; + break; + + case ColorOrder::BGRW: + _idxB=0; _idxG=1; _idxR=2; _idxW=3; + break; + + case ColorOrder::WRGB: + _idxW=0; _idxR=1; _idxG=2; _idxB=3; + break; + + case ColorOrder::WGRB: + _idxW=0; _idxG=1; _idxR=2; _idxB=3; + break; + + case ColorOrder::WBRG: + _idxW=0; _idxB=1; _idxR=2; _idxG=3; + break; + + case ColorOrder::WRBG: + _idxW=0; _idxR=1; _idxB=2; _idxG=3; + break; + + case ColorOrder::WGBR: + _idxW=0; _idxG=1; _idxB=2; _idxR=3; + break; + + case ColorOrder::WBGR: + _idxW=0; _idxB=1; _idxG=2; _idxR=3; + break; + + case ColorOrder::RGBWC: + _idxR=0; _idxG=1; _idxB=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::GRBWC: + _idxG=0; _idxR=1; _idxB=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::BRGWC: + _idxB=0; _idxR=1; _idxG=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::RBGWC: + _idxR=0; _idxB=1; _idxG=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::GBRWC: + _idxG=0; _idxB=1; _idxR=2; _idxWW=3; _idxCW=4; + break; + + case ColorOrder::BGRWC: + _idxB=0; _idxG=1; _idxR=2; _idxWW=3; _idxCW=4; + break; + + default: + _idxG=0; _idxR=1; _idxB=2; + break; + } } @@ -147,77 +147,77 @@ ColorEncoder::ColorEncoder(ColorOrder order) : _order(order), _numChannels(getCh //============================================================================== IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, - ColorOrder order, size_t bufferSize, int8_t channel) { - - Serial.printf("[WPB] createBus type=%u pin=%d bufSize=%u ch=%d\n", (unsigned)type, pin, bufferSize, channel); - if (type == BusType::Auto) { - type = getRecommendedBusType(); // TODO: when is "auto" used? should auto default to RMT? it currently defaults to I2S - Serial.printf("[WPB] Auto resolved to type=%u\n", (unsigned)type); - } + ColorOrder order, size_t bufferSize, int8_t channel) { + + Serial.printf("[WPB] createBus type=%u pin=%d bufSize=%u ch=%d\n", (unsigned)type, pin, bufferSize, channel); + if (type == BusType::Auto) { + type = getRecommendedBusType(); // TODO: when is "auto" used? should auto default to RMT? it currently defaults to I2S + Serial.printf("[WPB] Auto resolved to type=%u\n", (unsigned)type); + } - IBus* bus = nullptr; + IBus* bus = nullptr; - switch (type) { + switch (type) { #if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) - case BusType::RMT: - bus = new RmtBus(pin, timing, order, channel); - break; + case BusType::RMT: + bus = new RmtBus(pin, timing, order, channel); + break; #ifdef WLEDPB_I2S_SUPPORT - case BusType::I2S: + case BusType::I2S: #if defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32C3) - bus = new I2sBus(pin, timing, order, 0, bufferSize); + bus = new I2sBus(pin, timing, order, 0, bufferSize); #else - bus = new I2sBus(pin, timing, order, 1, bufferSize); + bus = new I2sBus(pin, timing, order, 1, bufferSize); #endif - break; + break; #endif #ifdef WLEDPB_LCD_SUPPORT - case BusType::LCD: - bus = new LcdBus(pin, timing, order, bufferSize); - break; + case BusType::LCD: + bus = new LcdBus(pin, timing, order, bufferSize); + break; #endif #ifdef WLEDPB_SPI_SUPPORT - case BusType::SPI: - bus = new ParallelSpiBus(pin, timing, order); - break; + case BusType::SPI: + bus = new ParallelSpiBus(pin, timing, order); + break; #endif #elif defined(WLEDPB_ESP8266) - case BusType::UART: - bus = new Esp8266UartBus(pin, timing, order); - break; - case BusType::DMA: - bus = new Esp8266DmaBus(pin, timing, order); - break; - case BusType::BitBang: - bus = new Esp8266BitBangBus(pin, timing, order); - break; + case BusType::UART: + bus = new Esp8266UartBus(pin, timing, order); + break; + case BusType::DMA: + bus = new Esp8266DmaBus(pin, timing, order); + break; + case BusType::BitBang: + bus = new Esp8266BitBangBus(pin, timing, order); + break; #endif - default: - return nullptr; - } + default: + return nullptr; + } - return bus; + return bus; } // TODO: this function is not really needed, bus type defaults should be handled by busmanager or buswrapper BusType getRecommendedBusType() { /* #if defined(WLEDPB_ESP32S3) - return BusType::LCD; // S3 has LCD support + return BusType::LCD; // S3 has LCD support #elif defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) - return BusType::I2S; // Original and S2 have I2S parallel + return BusType::I2S; // Original and S2 have I2S parallel #elif defined(WLEDPB_SPI_SUPPORT) - return BusType::SPI; // C3 uses SPI quad mode - */ + return BusType::SPI; // C3 uses SPI quad mode + */ #if defined(WLEDPB_ESP8266) - return BusType::BitBang; // use bitbanging by default (most versatile) + return BusType::BitBang; // use bitbanging by default (most versatile) #else - return BusType::RMT; // use RMT by default (most versatile) + return BusType::RMT; // use RMT by default (most versatile) #endif } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 0f1ca25265..3b9cb50fa0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -31,28 +31,28 @@ by @dedehai, 2026 // Platform detection #if defined(CONFIG_IDF_TARGET_ESP32) - #define WLEDPB_ESP32 + #define WLEDPB_ESP32 #elif defined(CONFIG_IDF_TARGET_ESP32S2) - #define WLEDPB_ESP32S2 + #define WLEDPB_ESP32S2 #elif defined(CONFIG_IDF_TARGET_ESP32S3) - #define WLEDPB_ESP32S3 + #define WLEDPB_ESP32S3 #elif defined(CONFIG_IDF_TARGET_ESP32C3) - #define WLEDPB_ESP32C3 + #define WLEDPB_ESP32C3 #endif // I2S support (ESP32 and S2 only for parallel mode) #if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) - #define WLEDPB_I2S_SUPPORT + #define WLEDPB_I2S_SUPPORT #endif // LCD support (S3 only) #if defined(WLEDPB_ESP32S3) - #define WLEDPB_LCD_SUPPORT + #define WLEDPB_LCD_SUPPORT #endif // SPI parallel support (C3 - uses SPI quad mode with GDMA) #if defined(WLEDPB_ESP32C3) - #define WLEDPB_SPI_SUPPORT + #define WLEDPB_SPI_SUPPORT #endif #ifdef WLEDPB_SPI_SUPPORT @@ -72,6 +72,16 @@ by @dedehai, 2026 #endif #include "WLEDpixelBus_Timings.h" +#include "WLEDpixelBus_SPI.h" + +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_LCD.h" +#include "WLEDpixelBus_ParallelSpi.h" +#elif defined(WLEDPB_ESP8266) +#include "WLEDpixelBus_ESP8266.h" +#endif namespace WLEDpixelBus { @@ -89,47 +99,47 @@ namespace WLEDpixelBus { //============================================================================== enum class ColorOrder : uint8_t { - // RGB variants (3 bytes) - GRB = 0, - RGB = 1, - BRG = 2, - RBG = 3, - BGR = 4, - GBR = 5, - // RGBW variants (4 bytes) - GRBW = 10, - RGBW = 11, - BRGW = 12, - RBGW = 13, - BGRW = 14, - GBRW = 15, - WRGB = 16, - WGRB = 17, - WBRG = 18, - WRBG = 19, - WGBR = 20, - WBGR = 21, - // RGBCWCW variants (5 bytes) - GRBWC = 30, - RGBWC = 31, - BRGWC = 32, - RBGWC = 33, - BGRWC = 34, - GBRWC = 35, + // RGB variants (3 bytes) + GRB = 0, + RGB = 1, + BRG = 2, + RBG = 3, + BGR = 4, + GBR = 5, + // RGBW variants (4 bytes) + GRBW = 10, + RGBW = 11, + BRGW = 12, + RBGW = 13, + BGRW = 14, + GBRW = 15, + WRGB = 16, + WGRB = 17, + WBRG = 18, + WRBG = 19, + WGBR = 20, + WBGR = 21, + // RGBCWCW variants (5 bytes) + GRBWC = 30, + RGBWC = 31, + BRGWC = 32, + RBGWC = 33, + BGRWC = 34, + GBRWC = 35, }; /** * Get byte count per pixel for a color order */ inline constexpr uint8_t getChannelCount(ColorOrder order) { - return (static_cast(order) >= 30) ? 5 : ((static_cast(order) >= 10) ? 4 : 3); + return (static_cast(order) >= 30) ? 5 : ((static_cast(order) >= 10) ? 4 : 3); } /** * Check if color order includes white channel */ inline constexpr bool hasWhiteChannel(ColorOrder order) { - return static_cast(order) >= 10; + return static_cast(order) >= 10; } //============================================================================== @@ -149,7 +159,7 @@ inline uint8_t getW(uint32_t color) { return (color >> 24) & 0xFF; } * Create uint32_t color from components */ inline uint32_t makeColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { - return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; + return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; } //============================================================================== @@ -161,8 +171,8 @@ inline uint32_t makeColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { * Passed as separate buffer from main RGBW data */ struct CctPixel { - uint8_t ww; // Warm white - uint8_t cw; // Cool white + uint8_t ww; // Warm white + uint8_t cw; // Cool white }; //============================================================================== @@ -170,10 +180,10 @@ struct CctPixel { //============================================================================== enum class DriverState : uint8_t { - Idle = 0, - Sending = 1, - SendingLast = 2, // Last data buffer was filled; wait for current buffer to finish so last-data buffer plays - WaitingReset = 3 // Last data buffer played; zero buffer playing as reset signal + Idle = 0, + Sending = 1, + SendingLast = 2, // Last data buffer was filled; wait for current buffer to finish so last-data buffer plays + WaitingReset = 3 // Last data buffer played; zero buffer playing as reset signal }; //============================================================================== @@ -190,64 +200,64 @@ constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; class IBus { protected: - uint32_t* _pixelData = nullptr; - CctPixel* _cctData = nullptr; - uint16_t _numPixels = 0; - uint8_t _brightness = 255; + uint32_t* _pixelData = nullptr; + CctPixel* _cctData = nullptr; + uint16_t _numPixels = 0; + uint8_t _brightness = 255; public: - virtual ~IBus() { - if (_pixelData) free(_pixelData); - if (_cctData) free(_cctData); + virtual ~IBus() { + if (_pixelData) free(_pixelData); + if (_cctData) free(_cctData); + } + + virtual bool begin() = 0; + virtual void end() = 0; + + /** + * Show pixels + * @param pixels RGBW pixel data (uint32_t per pixel, WLED format) + * @param numPixels Number of pixels + * @param cct Optional CCT data (2 bytes per pixel: WW, CW) + * @return true if transmission started successfully + */ + virtual bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, + const CctPixel* cct = nullptr) = 0; + + virtual bool canShow() const = 0; + virtual void waitComplete() = 0; + virtual const char* getType() const = 0; + + virtual bool allocatePixelBuffer(uint16_t numPixels, bool hasCCT = false) { + if (_pixelData) { + if (_numPixels == numPixels) return true; + free(_pixelData); + _pixelData = nullptr; } - - virtual bool begin() = 0; - virtual void end() = 0; - - /** - * Show pixels - * @param pixels RGBW pixel data (uint32_t per pixel, WLED format) - * @param numPixels Number of pixels - * @param cct Optional CCT data (2 bytes per pixel: WW, CW) - * @return true if transmission started successfully - */ - virtual bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, - const CctPixel* cct = nullptr) = 0; - - virtual bool canShow() const = 0; - virtual void waitComplete() = 0; - virtual const char* getType() const = 0; - - virtual bool allocatePixelBuffer(uint16_t numPixels, bool hasCCT = false) { - if (_pixelData) { - if (_numPixels == numPixels) return true; - free(_pixelData); - _pixelData = nullptr; - } - if (_cctData) { - free(_cctData); - _cctData = nullptr; - } - _numPixels = numPixels; - if (numPixels == 0) return true; - // TODO: use WLED alloc functions that are min-heap safe - _pixelData = (uint32_t*)malloc(numPixels * sizeof(uint32_t)); - if (!_pixelData) return false; - memset(_pixelData, 0, numPixels * sizeof(uint32_t)); - - if (hasCCT) { - _cctData = (CctPixel*)malloc(numPixels * sizeof(CctPixel)); - if (!_cctData) { - free(_pixelData); - _pixelData = nullptr; - return false; - } - if (_cctData) memset(_cctData, 0, numPixels * sizeof(CctPixel)); - } - - return true; + if (_cctData) { + free(_cctData); + _cctData = nullptr; + } + _numPixels = numPixels; + if (numPixels == 0) return true; + // TODO: use WLED alloc functions that are min-heap safe + _pixelData = (uint32_t*)malloc(numPixels * sizeof(uint32_t)); + if (!_pixelData) return false; + memset(_pixelData, 0, numPixels * sizeof(uint32_t)); + + if (hasCCT) { + _cctData = (CctPixel*)malloc(numPixels * sizeof(CctPixel)); + if (!_cctData) { + free(_pixelData); + _pixelData = nullptr; + return false; + } + if (_cctData) memset(_cctData, 0, numPixels * sizeof(CctPixel)); } + return true; + } + // TODO: can be removed, pixels are now written to the buffer directly in busDigital::setPixelColor() // virtual void IRAM_ATTR setPixelColor(uint16_t pix, uint32_t c, const CctPixel* cp = nullptr) { // if (pix >= _numPixels) return; // TODO: can this be removed safely? busmanager already checks? its in the hot path. @@ -255,21 +265,21 @@ class IBus { // if (cp && _cctData) _cctData[pix] = *cp; // TODO: make set cct a seperate function? might speed things up by removing this check // } - virtual uint32_t getPixelColor(uint16_t pix) const { - if (pix >= _numPixels) return 0; - return _pixelData[pix]; - } - - virtual uint16_t getNumPixels() const { - return _numPixels; - } - - virtual void setBrightness(uint8_t b) { - _brightness = b; - } - - virtual uint32_t* getPixelData() { return _pixelData; } - virtual CctPixel* getCctData() { return _cctData; } + virtual uint32_t getPixelColor(uint16_t pix) const { + if (pix >= _numPixels) return 0; + return _pixelData[pix]; + } + + virtual uint16_t getNumPixels() const { + return _numPixels; + } + + virtual void setBrightness(uint8_t b) { + _brightness = b; + } + + virtual uint32_t* getPixelData() { return _pixelData; } + virtual CctPixel* getCctData() { return _cctData; } }; //============================================================================== @@ -297,45 +307,45 @@ class LcdBusContext; */ class ColorEncoder { public: - ColorEncoder(ColorOrder order); + ColorEncoder(ColorOrder order); - /** - * Encode a single pixel to output buffer - * @param pixel RGBW color value - * @param cct Optional CCT data (for CCT strips, replaces W channel) - * @param out Output buffer (must have space for numChannels bytes) - */ - inline void encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const; + /** + * Encode a single pixel to output buffer + * @param pixel RGBW color value + * @param cct Optional CCT data (for CCT strips, replaces W channel) + * @param out Output buffer (must have space for numChannels bytes) + */ + inline void encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const; - uint8_t getNumChannels() const { return _numChannels; } - ColorOrder getOrder() const { return _order; } + uint8_t getNumChannels() const { return _numChannels; } + ColorOrder getOrder() const { return _order; } private: - ColorOrder _order; - uint8_t _numChannels; - uint8_t _idxR; - uint8_t _idxG; - uint8_t _idxB; - uint8_t _idxW; - uint8_t _idxWW; - uint8_t _idxCW; + ColorOrder _order; + uint8_t _numChannels; + uint8_t _idxR; + uint8_t _idxG; + uint8_t _idxB; + uint8_t _idxW; + uint8_t _idxWW; + uint8_t _idxCW; }; inline void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* out) const { - uint8_t r = getR(pixel); - uint8_t g = getG(pixel); - uint8_t b = getB(pixel); - uint8_t w = getW(pixel); - - out[_idxR] = r; - out[_idxG] = g; - out[_idxB] = b; - if (_idxW != 0xFF) out[_idxW] = w; - - if (_idxWW != 0xFF) { - out[_idxWW] = cct ? cct->ww : w; // TODO: handle ww/cw more clever, there is not really a need to have this explicit, could save an if for RGB case - out[_idxCW] = cct ? cct->cw : 0; - } + uint8_t r = getR(pixel); + uint8_t g = getG(pixel); + uint8_t b = getB(pixel); + uint8_t w = getW(pixel); + + out[_idxR] = r; + out[_idxG] = g; + out[_idxB] = b; + if (_idxW != 0xFF) out[_idxW] = w; + + if (_idxWW != 0xFF) { + out[_idxWW] = cct ? cct->ww : w; // TODO: handle ww/cw more clever, there is not really a need to have this explicit, could save an if for RGB case + out[_idxCW] = cct ? cct->cw : 0; + } } @@ -345,14 +355,14 @@ inline void ColorEncoder::encode(uint32_t pixel, const CctPixel* cct, uint8_t* o //============================================================================== enum class BusType : uint8_t { - RMT = 0, - I2S = 1, - LCD = 2, - SPI = 3, - UART = 4, - DMA = 5, - BitBang = 6, - Auto = 255 + RMT = 0, + I2S = 1, + LCD = 2, + SPI = 3, + UART = 4, + DMA = 5, + BitBang = 6, + Auto = 255 }; /** @@ -360,13 +370,13 @@ enum class BusType : uint8_t { */ constexpr uint8_t getRmtMaxChannels() { #if defined(WLEDPB_ESP32) - return 8; // ESP32 original: 8 RMT channels + return 8; // ESP32 original: 8 RMT channels #elif defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) - return 4; // ESP32-S2/S3: 4 RMT TX channels + return 4; // ESP32-S2/S3: 4 RMT TX channels #elif defined(WLEDPB_ESP32C3) - return 2; // ESP32-C3: 2 RMT TX channels + return 2; // ESP32-C3: 2 RMT TX channels #else - return 0; + return 0; #endif } @@ -381,8 +391,8 @@ constexpr uint8_t getRmtMaxChannels() { * @return Bus instance (caller owns, delete when done) */ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, - ColorOrder order, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, - int8_t channel = -1); + ColorOrder order, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, + int8_t channel = -1); /** * Get recommended bus type for current platform @@ -395,52 +405,42 @@ BusType getRecommendedBusType(); */ // TODO: check if this is accurate and busmanager does not double-account for things calculated here static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t channelCount, size_t dmaBufferSize = DEFAULT_DMA_BUFFER_SIZE) { - size_t mem = 0; - - // Bus instance overhead - mem += 128; // Approximate C++ object size + IBus data structures - // TODO: check if this is correct - switch (type) { - case BusType::UART: - // UART encode buffer: 4 encoded bytes per LED byte (no DMA) - mem += numPixels * channelCount * 4; - break; - - case BusType::BitBang: - mem += numPixels * channelCount; // single buffer - break; - - case BusType::DMA: - // I2S DMA on ESP8266: same 4-step encoding as UART but written via DMA - mem += numPixels * channelCount * 4; - break; - - default: - // Encode buffer (RMT, I2S, LCD, SPI, etc.) - mem += numPixels * channelCount; - - // DMA buffers and descriptors for I2S/LCD - // They are double-buffered and mapped via heap_caps_malloc - if (type == BusType::I2S || type == BusType::LCD || type == BusType::Auto) { - mem += dmaBufferSize * 2; - // Include minimal descriptor memory estimates (~64 bytes per context) - mem += 64; - } - break; - } - - return mem; + size_t mem = 0; + + // Bus instance overhead + mem += 128; // Approximate C++ object size + IBus data structures + // TODO: check if this is correct + switch (type) { + case BusType::UART: + // UART encode buffer: 4 encoded bytes per LED byte (no DMA) + mem += numPixels * channelCount * 4; + break; + + case BusType::BitBang: + mem += numPixels * channelCount; // single buffer + break; + + case BusType::DMA: + // I2S DMA on ESP8266: same 4-step encoding as UART but written via DMA + mem += numPixels * channelCount * 4; + break; + + default: + // Encode buffer (RMT, I2S, LCD, SPI, etc.) + mem += numPixels * channelCount; + + // DMA buffers and descriptors for I2S/LCD + // They are double-buffered and mapped via heap_caps_malloc + if (type == BusType::I2S || type == BusType::LCD || type == BusType::Auto) { + mem += dmaBufferSize * 2; + // Include minimal descriptor memory estimates (~64 bytes per context) + mem += 64; + } + break; + } + + return mem; } } // namespace WLEDpixelBus -#include "WLEDpixelBus_SPI.h" - -#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) -#include "WLEDpixelBus_RMT.h" -#include "WLEDpixelBus_I2S.h" -#include "WLEDpixelBus_LCD.h" -#include "WLEDpixelBus_ParallelSpi.h" -#elif defined(WLEDPB_ESP8266) -#include "WLEDpixelBus_ESP8266.h" -#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index 369d907f1a..2d36b6ab6c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -23,120 +23,120 @@ namespace WLEDpixelBus { //============================================================================== Esp8266UartBus::Esp8266UartBus(int8_t pin, const LedTiming& timing, ColorOrder order) - : _pin(pin), _timing(timing), _order(order), _initialized(false), _encodeLut(nullptr), _encodeBuffer(nullptr), _encodeBufferSize(0) {} + : _pin(pin), _timing(timing), _order(order), _initialized(false), _encodeLut(nullptr), _encodeBuffer(nullptr), _encodeBufferSize(0) {} Esp8266UartBus::~Esp8266UartBus() { - end(); - if (_encodeLut) free(_encodeLut); - if (_encodeBuffer) free(_encodeBuffer); + end(); + if (_encodeLut) free(_encodeLut); + if (_encodeBuffer) free(_encodeBuffer); } void Esp8266UartBus::buildLut() { - if (!_encodeLut) _encodeLut = (uint8_t*)malloc(256 * 4); // TODO: this is really bad memory management! should only have a pixel buffer and do encoding on the fly - if (!_encodeLut) return; - const uint8_t uartData[4] = {0b110111, 0b000111, 0b110100, 0b000100}; - for (int i=0; i<256; i++) { - _encodeLut[i*4 + 0] = uartData[(i >> 6) & 0x03]; - _encodeLut[i*4 + 1] = uartData[(i >> 4) & 0x03]; - _encodeLut[i*4 + 2] = uartData[(i >> 2) & 0x03]; - _encodeLut[i*4 + 3] = uartData[i & 0x03]; - } + if (!_encodeLut) _encodeLut = (uint8_t*)malloc(256 * 4); // TODO: this is really bad memory management! should only have a pixel buffer and do encoding on the fly + if (!_encodeLut) return; + const uint8_t uartData[4] = {0b110111, 0b000111, 0b110100, 0b000100}; + for (int i=0; i<256; i++) { + _encodeLut[i*4 + 0] = uartData[(i >> 6) & 0x03]; + _encodeLut[i*4 + 1] = uartData[(i >> 4) & 0x03]; + _encodeLut[i*4 + 2] = uartData[(i >> 2) & 0x03]; + _encodeLut[i*4 + 3] = uartData[i & 0x03]; + } } bool Esp8266UartBus::allocateBuffer(size_t encodedDataLen) { - if (_encodeBufferSize >= encodedDataLen) return true; - if (_encodeBuffer) free(_encodeBuffer); - _encodeBuffer = (uint8_t*)malloc(encodedDataLen); - if (!_encodeBuffer) return false; - _encodeBufferSize = encodedDataLen; - return true; + if (_encodeBufferSize >= encodedDataLen) return true; + if (_encodeBuffer) free(_encodeBuffer); + _encodeBuffer = (uint8_t*)malloc(encodedDataLen); + if (!_encodeBuffer) return false; + _encodeBufferSize = encodedDataLen; + return true; } bool Esp8266UartBus::begin() { - if (_initialized) return true; - if (_pin != 1 && _pin != 2) return false; - buildLut(); - updateUartTiming(); - _initialized = true; - return true; + if (_initialized) return true; + if (_pin != 1 && _pin != 2) return false; + buildLut(); + updateUartTiming(); + _initialized = true; + return true; } void Esp8266UartBus::end() { - if (!_initialized) return; - if (_pin == 2) Serial1.end(); - else Serial.end(); - _initialized = false; + if (!_initialized) return; + 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); - } - - // Clear the RX & TX FIFOS - const uint32_t fifoResetFlags = (1 << UCTXRST) | (1 << UCRXRST); - USC0(uartNum) |= fifoResetFlags; - USC0(uartNum) &= ~(fifoResetFlags); - - // clear all invert bits - USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI) | (1 << UCTXI)); - - // Disable RX & TX interrupts that might have been enabled by Arduino Core Serial -> needed? - //USIE(uartNum) &= ~((1 << UIFF) | (1 << UIFE)); + 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); + } + + // Clear the RX & TX FIFOS + const uint32_t fifoResetFlags = (1 << UCTXRST) | (1 << UCRXRST); + USC0(uartNum) |= fifoResetFlags; + USC0(uartNum) &= ~(fifoResetFlags); + + // clear all invert bits + USC0(uartNum) &= ~((1 << UCDTRI) | (1 << UCRTSI) | (1 << UCTXI) | (1 << UCDSRI) | (1 << UCCTSI) | (1 << UCRXI) | (1 << UCTXI)); + + // Disable RX & TX interrupts that might have been enabled by Arduino Core Serial -> needed? + //USIE(uartNum) &= ~((1 << UIFF) | (1 << UIFE)); } bool Esp8266UartBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } - if (!_initialized || !pixels || numPixels == 0) return false; - - size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); - size_t outLen = numPixels * bpp * 4; // TODO: should not be *4, we can encode on the fly if done right. - if (!allocateBuffer(outLen)) return false; - - ColorEncoder encoder(_order); - uint8_t* out = _encodeBuffer; - uint8_t pData[5]; - - for (size_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); - for (size_t b = 0; b < bpp; b++) { - uint8_t v = pData[b]; - *out++ = _encodeLut[v * 4 + 0]; - *out++ = _encodeLut[v * 4 + 1]; - *out++ = _encodeLut[v * 4 + 2]; - *out++ = _encodeLut[v * 4 + 3]; - } + if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } + if (!_initialized || !pixels || numPixels == 0) return false; + + size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); + size_t outLen = numPixels * bpp * 4; // TODO: should not be *4, we can encode on the fly if done right. + if (!allocateBuffer(outLen)) return false; + + ColorEncoder encoder(_order); + uint8_t* out = _encodeBuffer; + uint8_t pData[5]; + + for (size_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); + for (size_t b = 0; b < bpp; b++) { + uint8_t v = pData[b]; + *out++ = _encodeLut[v * 4 + 0]; + *out++ = _encodeLut[v * 4 + 1]; + *out++ = _encodeLut[v * 4 + 2]; + *out++ = _encodeLut[v * 4 + 3]; } - - uint8_t uartNum = (_pin == 2) ? 1 : 0; - out = _encodeBuffer; - size_t len = outLen; - - // Busy wait UART filling to ensure stable rendering (OS interrupts intact but tight timing loop) - while(len > 0) { - if (((USS(uartNum) >> USTXC) & 0xff) < 127) { - USF(uartNum) = *out++; - len--; - } + } + + uint8_t uartNum = (_pin == 2) ? 1 : 0; + out = _encodeBuffer; + size_t len = outLen; + + // Busy wait UART filling to ensure stable rendering (OS interrupts intact but tight timing loop) + while(len > 0) { + if (((USS(uartNum) >> USTXC) & 0xff) < 127) { + USF(uartNum) = *out++; + len--; } - return true; + } + return true; } bool Esp8266UartBus::canShow() const { - if (!_initialized) return false; - uint8_t uartNum = (_pin == 2) ? 1 : 0; - return (((USS(uartNum) >> USTXC) & 0xff) == 0); // ready when FIFO empty + if (!_initialized) return false; + uint8_t uartNum = (_pin == 2) ? 1 : 0; + return (((USS(uartNum) >> USTXC) & 0xff) == 0); // ready when FIFO empty } void Esp8266UartBus::waitComplete() { - while (!canShow()) { yield(); } + while (!canShow()) { yield(); } } @@ -145,75 +145,75 @@ void Esp8266UartBus::waitComplete() { //============================================================================== Esp8266BitBangBus::Esp8266BitBangBus(int8_t pin, const LedTiming& timing, ColorOrder order) - : _pin(pin), _timing(timing), _order(order), _initialized(false) {} + : _pin(pin), _timing(timing), _order(order), _initialized(false) {} Esp8266BitBangBus::~Esp8266BitBangBus() { - end(); + end(); } bool Esp8266BitBangBus::begin() { - if (_initialized) return true; - pinMode(_pin, OUTPUT); - digitalWrite(_pin, LOW); - setTiming(_timing); - _initialized = true; - return true; + if (_initialized) return true; + pinMode(_pin, OUTPUT); + digitalWrite(_pin, LOW); + setTiming(_timing); + _initialized = true; + return true; } void Esp8266BitBangBus::end() { - pinMode(_pin, INPUT); - _initialized = false; + pinMode(_pin, INPUT); + _initialized = false; } void Esp8266BitBangBus::setTiming(const LedTiming& timing) { - _timing = timing; - uint32_t cpuFreq = ESP.getCpuFreqMHz(); // usually 80 or 160 - _t0h = (timing.t0h_ns * cpuFreq) / 1000; - _t0l = (timing.t0l_ns * cpuFreq) / 1000; - _t1h = (timing.t1h_ns * cpuFreq) / 1000; - _t1l = (timing.t1l_ns * cpuFreq) / 1000; + _timing = timing; + uint32_t cpuFreq = ESP.getCpuFreqMHz(); // usually 80 or 160 + _t0h = (timing.t0h_ns * cpuFreq) / 1000; + _t0l = (timing.t0l_ns * cpuFreq) / 1000; + _t1h = (timing.t1h_ns * cpuFreq) / 1000; + _t1l = (timing.t1l_ns * cpuFreq) / 1000; } bool Esp8266BitBangBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } - if (!_initialized || !pixels || numPixels == 0) return false; - - size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); - ColorEncoder encoder(_order); - uint32_t mask = 1 << _pin; - uint8_t pData[5]; - - os_intr_lock(); - for (size_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); - for (size_t b = 0; b < bpp; b++) { - uint8_t v = pData[b]; - for (int bit = 7; bit >= 0; bit--) { - uint32_t t = ESP.getCycleCount(); - if (v & (1 << bit)) { - GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, mask); - while((ESP.getCycleCount() - t) < _t1h); - GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, mask); - while((ESP.getCycleCount() - t) < (_t1h + _t1l)); - } else { - GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, mask); - while((ESP.getCycleCount() - t) < _t0h); - GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, mask); - while((ESP.getCycleCount() - t) < (_t0h + _t0l)); - } - } + if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } + if (!_initialized || !pixels || numPixels == 0) return false; + + size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); + ColorEncoder encoder(_order); + uint32_t mask = 1 << _pin; + uint8_t pData[5]; + + os_intr_lock(); + for (size_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); + for (size_t b = 0; b < bpp; b++) { + uint8_t v = pData[b]; + for (int bit = 7; bit >= 0; bit--) { + uint32_t t = ESP.getCycleCount(); + if (v & (1 << bit)) { + GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, mask); + while((ESP.getCycleCount() - t) < _t1h); + GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, mask); + while((ESP.getCycleCount() - t) < (_t1h + _t1l)); + } else { + GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, mask); + while((ESP.getCycleCount() - t) < _t0h); + GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, mask); + while((ESP.getCycleCount() - t) < (_t0h + _t0l)); } + } } - os_intr_unlock(); - return true; + } + os_intr_unlock(); + return true; } bool Esp8266BitBangBus::canShow() const { - return _initialized; + return _initialized; } void Esp8266BitBangBus::waitComplete() { - // Blocking show, always complete upon return + // Blocking show, always complete upon return } @@ -222,119 +222,119 @@ void Esp8266BitBangBus::waitComplete() { //============================================================================== Esp8266DmaBus::Esp8266DmaBus(int8_t pin, const LedTiming& timing, ColorOrder order) - : _pin(pin), _timing(timing), _order(order), _initialized(false), _encodeBuffer(nullptr), _encodeBufferSize(0) {} + : _pin(pin), _timing(timing), _order(order), _initialized(false), _encodeBuffer(nullptr), _encodeBufferSize(0) {} Esp8266DmaBus::~Esp8266DmaBus() { - end(); - if (_encodeBuffer) { - free(_encodeBuffer); - _encodeBuffer = nullptr; - } + end(); + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + } } bool Esp8266DmaBus::allocateBuffer(size_t len) { - if (_encodeBufferSize >= len) return true; - if (_encodeBuffer) free(_encodeBuffer); - _encodeBuffer = (uint8_t*)malloc(len); - if (!_encodeBuffer) return false; - _encodeBufferSize = len; - return true; + if (_encodeBufferSize >= len) return true; + if (_encodeBuffer) free(_encodeBuffer); + _encodeBuffer = (uint8_t*)malloc(len); + if (!_encodeBuffer) return false; + _encodeBufferSize = len; + return true; } void Esp8266DmaBus::updateI2sTiming() { - // Setup using Native ESP8266 Core I2S - uint32_t bitPeriod = _timing.bitPeriod(); - if (bitPeriod == 0) bitPeriod = 1250; - - // We map 4 I2S bits to 1 LED bit (4-step cadence). - // Each LED bit needs 4 I2S periods, thus our desired I2S clock is 4x the LED bit rate. - // periodNs is time per LED bit. I2S bit clock period = bitPeriod / 4. - // I2S bits/sec = 1,000,000,000 / (bitPeriod / 4) = 4,000,000,000 / bitPeriod. - // Using core `i2s_begin` which sets a clock and routing: - // Actually, `i2s_begin` expects a sample rate for 32-bit (stereo 16+16) frames. - // Sample rate = (I2S bits/sec) / 32 - uint32_t sampleRate = (4000000000ULL / bitPeriod) / 32; - i2s_set_rate(sampleRate); + // Setup using Native ESP8266 Core I2S + uint32_t bitPeriod = _timing.bitPeriod(); + if (bitPeriod == 0) bitPeriod = 1250; + + // We map 4 I2S bits to 1 LED bit (4-step cadence). + // Each LED bit needs 4 I2S periods, thus our desired I2S clock is 4x the LED bit rate. + // periodNs is time per LED bit. I2S bit clock period = bitPeriod / 4. + // I2S bits/sec = 1,000,000,000 / (bitPeriod / 4) = 4,000,000,000 / bitPeriod. + // Using core `i2s_begin` which sets a clock and routing: + // Actually, `i2s_begin` expects a sample rate for 32-bit (stereo 16+16) frames. + // Sample rate = (I2S bits/sec) / 32 + uint32_t sampleRate = (4000000000ULL / bitPeriod) / 32; + i2s_set_rate(sampleRate); } bool Esp8266DmaBus::begin() { - if (_initialized) return true; - if (_pin != 3) return false; // ESP8266 I2S RX is GPIO3 for NeoPixels - - // Begin core I2S subsystem without driving clocks on GPIO15/GPIO2! - i2s_rxtxdrive_begin(false, true, false, false); + if (_initialized) return true; + if (_pin != 3) return false; // ESP8266 I2S RX is GPIO3 for NeoPixels + + // Begin core I2S subsystem without driving clocks on GPIO15/GPIO2! + i2s_rxtxdrive_begin(false, true, false, false); - // To make GPIO3 act as I2S Data Out instead of I2S IN, configure registers manually: - PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, 1); // Select I2SO_DATA on GPIO3 + // To make GPIO3 act as I2S Data Out instead of I2S IN, configure registers manually: + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, 1); // Select I2SO_DATA on GPIO3 - updateI2sTiming(); + updateI2sTiming(); - _initialized = true; - return true; + _initialized = true; + return true; } void Esp8266DmaBus::end() { - if (!_initialized) return; - i2s_end(); - pinMode(_pin, INPUT); - _initialized = false; + if (!_initialized) return; + i2s_end(); + pinMode(_pin, INPUT); + _initialized = false; } bool Esp8266DmaBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } - if (!_initialized || !pixels || numPixels == 0) return false; - - size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); - - // 4 step cadence per LED bit. - // 1 LED bit = 4 I2S bits (half-byte / nibble). - // 1 LED byte (8 bits) = 32 I2S bits (4 bytes). - size_t outLen = numPixels * bpp * 4 + 40; // + 40 zero bytes for reset (latch) - if (!allocateBuffer(outLen)) return false; - - memset(_encodeBuffer, 0, outLen); - - ColorEncoder encoder(_order); - uint32_t* out32 = (uint32_t*)_encodeBuffer; - uint8_t pData[5]; - - // Using inverted 4-step cadence: - // Normally: - // 1 = 0b1110 (0xE) - // 0 = 0b1000 (0x8) - for (size_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); - for (size_t b = 0; b < bpp; b++) { - uint32_t i2sData = 0; - uint8_t v = pData[b]; - for (int bit = 7; bit >= 0; bit--) { - i2sData <<= 4; - if (v & (1 << bit)) { - i2sData |= 0xE; // 1110 - } else { - i2sData |= 0x8; // 1000 - } - } - *out32++ = i2sData; + if (!pixels) { pixels = _pixelData; numPixels = _numPixels; cct = _cctData; } + if (!_initialized || !pixels || numPixels == 0) return false; + + size_t bpp = (_order >= ColorOrder::RGBWC) ? 5 : ((_order >= ColorOrder::RGBW) ? 4 : 3); + + // 4 step cadence per LED bit. + // 1 LED bit = 4 I2S bits (half-byte / nibble). + // 1 LED byte (8 bits) = 32 I2S bits (4 bytes). + size_t outLen = numPixels * bpp * 4 + 40; // + 40 zero bytes for reset (latch) + if (!allocateBuffer(outLen)) return false; + + memset(_encodeBuffer, 0, outLen); + + ColorEncoder encoder(_order); + uint32_t* out32 = (uint32_t*)_encodeBuffer; + uint8_t pData[5]; + + // Using inverted 4-step cadence: + // Normally: + // 1 = 0b1110 (0xE) + // 0 = 0b1000 (0x8) + for (size_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, pData); + for (size_t b = 0; b < bpp; b++) { + uint32_t i2sData = 0; + uint8_t v = pData[b]; + for (int bit = 7; bit >= 0; bit--) { + i2sData <<= 4; + if (v & (1 << bit)) { + i2sData |= 0xE; // 1110 + } else { + i2sData |= 0x8; // 1000 } + } + *out32++ = i2sData; } + } - // Write using Core I2S: It processes full `uint32` buffer natively over I2S - uint32_t* buf32 = (uint32_t*)_encodeBuffer; - for (size_t i = 0; i < outLen / 4; i++) { - i2s_write_sample(buf32[i]); - } + // Write using Core I2S: It processes full `uint32` buffer natively over I2S + uint32_t* buf32 = (uint32_t*)_encodeBuffer; + for (size_t i = 0; i < outLen / 4; i++) { + i2s_write_sample(buf32[i]); + } - return true; + return true; } bool Esp8266DmaBus::canShow() const { - return _initialized && !i2s_is_full(); + return _initialized && !i2s_is_full(); } void Esp8266DmaBus::waitComplete() { - // Not strictly supported blocking via I2S, wait for FIFO room - while (i2s_is_full()) { yield(); } + // Not strictly supported blocking via I2S, wait for FIFO room + while (i2s_is_full()) { yield(); } } } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index 23167e3e98..87940f4efd 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -12,33 +12,33 @@ namespace WLEDpixelBus { class Esp8266UartBus : public IBus { public: - Esp8266UartBus(int8_t pin, const LedTiming& timing, ColorOrder order); - ~Esp8266UartBus() override; + Esp8266UartBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~Esp8266UartBus() override; - bool begin() override; - void end() override; + bool begin() override; + void end() override; - bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "ESP8266_UART"; } + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "ESP8266_UART"; } - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order) { _order = order; } + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } private: - int8_t _pin; - LedTiming _timing; - ColorOrder _order; - bool _initialized; - uint8_t* _encodeLut; - - void updateUartTiming(); - bool allocateBuffer(size_t encodedDataLen); - void buildLut(); - - uint8_t* _encodeBuffer = nullptr; - size_t _encodeBufferSize = 0; + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + uint8_t* _encodeLut; + + void updateUartTiming(); + bool allocateBuffer(size_t encodedDataLen); + void buildLut(); + + uint8_t* _encodeBuffer = nullptr; + size_t _encodeBufferSize = 0; }; //============================================================================== @@ -47,31 +47,31 @@ class Esp8266UartBus : public IBus { class Esp8266DmaBus : public IBus { public: - Esp8266DmaBus(int8_t pin, const LedTiming& timing, ColorOrder order); - ~Esp8266DmaBus() override; + Esp8266DmaBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~Esp8266DmaBus() override; - bool begin() override; - void end() override; + bool begin() override; + void end() override; - bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "ESP8266_DMA"; } + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "ESP8266_DMA"; } - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order) { _order = order; } + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } private: - int8_t _pin; // Only RX pin (GPIO3) supported for I2S DMA on ESP8266 - LedTiming _timing; - ColorOrder _order; - bool _initialized; + int8_t _pin; // Only RX pin (GPIO3) supported for I2S DMA on ESP8266 + LedTiming _timing; + ColorOrder _order; + bool _initialized; - void updateI2sTiming(); - bool allocateBuffer(size_t numPixels); + void updateI2sTiming(); + bool allocateBuffer(size_t numPixels); - uint8_t* _encodeBuffer = nullptr; - size_t _encodeBufferSize = 0; + uint8_t* _encodeBuffer = nullptr; + size_t _encodeBufferSize = 0; }; //============================================================================== @@ -80,28 +80,28 @@ class Esp8266DmaBus : public IBus { class Esp8266BitBangBus : public IBus { public: - Esp8266BitBangBus(int8_t pin, const LedTiming& timing, ColorOrder order); - ~Esp8266BitBangBus() override; + Esp8266BitBangBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~Esp8266BitBangBus() override; - bool begin() override; - void end() override; + bool begin() override; + void end() override; - bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "ESP8266_BB"; } + bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "ESP8266_BB"; } - void setTiming(const LedTiming& timing); - void setColorOrder(ColorOrder order) { _order = order; } + void setTiming(const LedTiming& timing); + void setColorOrder(ColorOrder order) { _order = order; } private: - int8_t _pin; - LedTiming _timing; - ColorOrder _order; - bool _initialized; - - // Cycle counts - uint32_t _t0h, _t0l, _t1h, _t1l; + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; + + // Cycle counts + uint32_t _t0h, _t0l, _t1h, _t1l; }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h index 7f52132292..9d46aea42d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h @@ -14,57 +14,57 @@ namespace WLEDpixelBus { */ class FeaturePadder { public: - FeaturePadder() : _prefixLen(0) {} + FeaturePadder() : _prefixLen(0) {} - /** - * Set up the prefix for TM1814 - * Format is: 4 bytes of config + 4 bytes of inverted config - * Data: R_mA, G_mA, B_mA, W_mA (typically 225 = 0xE1 each for 22.5mA) - */ - void setupTM1814(uint8_t r_mA = 225, uint8_t g_mA = 225, uint8_t b_mA = 225, uint8_t w_mA = 225) { - _prefix[0] = r_mA; - _prefix[1] = g_mA; - _prefix[2] = b_mA; - _prefix[3] = w_mA; - _prefix[4] = ~r_mA; - _prefix[5] = ~g_mA; - _prefix[6] = ~b_mA; - _prefix[7] = ~w_mA; - _prefixLen = 8; - } + /** + * Set up the prefix for TM1814 + * Format is: 4 bytes of config + 4 bytes of inverted config + * Data: R_mA, G_mA, B_mA, W_mA (typically 225 = 0xE1 each for 22.5mA) + */ + void setupTM1814(uint8_t r_mA = 225, uint8_t g_mA = 225, uint8_t b_mA = 225, uint8_t w_mA = 225) { + _prefix[0] = r_mA; + _prefix[1] = g_mA; + _prefix[2] = b_mA; + _prefix[3] = w_mA; + _prefix[4] = ~r_mA; + _prefix[5] = ~g_mA; + _prefix[6] = ~b_mA; + _prefix[7] = ~w_mA; + _prefixLen = 8; + } - /** - * Set up the prefix for TM1914 - * Mode settings configuration. - * Mode 1: DIN/FDIN auto-switch (Default WLED setup) - * Format is typically 14 bytes (7 normal, 7 inverted) - */ - void setupTM1914() { - // Mode: DinFdinAutoSwitch - uint8_t modeData = 0x01; - - for (int i = 0; i < 7; i++) { - _prefix[i] = modeData; - _prefix[i + 7] = ~modeData; - } - _prefixLen = 14; + /** + * Set up the prefix for TM1914 + * Mode settings configuration. + * Mode 1: DIN/FDIN auto-switch (Default WLED setup) + * Format is typically 14 bytes (7 normal, 7 inverted) + */ + void setupTM1914() { + // Mode: DinFdinAutoSwitch + uint8_t modeData = 0x01; + + for (int i = 0; i < 7; i++) { + _prefix[i] = modeData; + _prefix[i + 7] = ~modeData; } + _prefixLen = 14; + } - bool hasPrefix() const { - return _prefixLen > 0; - } + bool hasPrefix() const { + return _prefixLen > 0; + } - uint8_t getPrefixLen() const { - return _prefixLen; - } + uint8_t getPrefixLen() const { + return _prefixLen; + } - const uint8_t* getPrefixData() const { - return _prefix; - } + const uint8_t* getPrefixData() const { + return _prefix; + } private: - uint8_t _prefix[16]; - uint8_t _prefixLen; + uint8_t _prefix[16]; + uint8_t _prefixLen; }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 6169abcfda..ac78d398e9 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -12,447 +12,447 @@ 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 (busNum >= WLEDPB_I2S_BUS_COUNT) return nullptr; - if (_instances[busNum] == nullptr) { - _instances[busNum] = new I2sBusContext(busNum); - } - _refCount[busNum]++; - return _instances[busNum]; + if (_instances[busNum] == nullptr) { + _instances[busNum] = new I2sBusContext(busNum); + } + _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; - } + 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; + } } I2sBusContext::I2sBusContext(uint8_t busNum) - : _busNum(busNum) - , _state(DriverState::Idle) - , _initialized(false) - , _bufferSize(0) - , _activeBuffer(0) - , _timing{0, 0, 0, 0, 0} - , _clockDiv(1) - , _isrHandle(nullptr) - , _channelCount(0) - , _channelMask(0) - , _stagedMask(0) - , _maxDataLen(0) + : _busNum(busNum) + , _state(DriverState::Idle) + , _initialized(false) + , _bufferSize(0) + , _activeBuffer(0) + , _timing{0, 0, 0, 0, 0} + , _clockDiv(1) + , _isrHandle(nullptr) + , _channelCount(0) + , _channelMask(0) + , _stagedMask(0) + , _maxDataLen(0) { #if defined(WLEDPB_ESP32) - _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; + _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; #else - _i2sDev = &I2S0; + _i2sDev = &I2S0; #endif - for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; - } + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } - _dmaDesc[0] = _dmaDesc[1] = nullptr; - _dmaBuffer[0] = _dmaBuffer[1] = nullptr; + _dmaDesc[0] = _dmaDesc[1] = nullptr; + _dmaBuffer[0] = _dmaBuffer[1] = nullptr; } I2sBusContext:: ~I2sBusContext() { - deinit(); + deinit(); } bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { - if (_initialized) return true; - - _timing = timing; - _bufferSize = (bufferSize + 3) & ~3; // align to 4 bytes - - // Allocate DMA buffers (4-byte aligned for DMA) - for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - Serial.println("I2S DMA buffer alloc failed"); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, _bufferSize); - - _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - Serial.println("I2S DMA desc alloc failed"); - deinit(); - return false; - } + if (_initialized) return true; + + _timing = timing; + _bufferSize = (bufferSize + 3) & ~3; // align to 4 bytes + + // Allocate DMA buffers (4-byte aligned for DMA) + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + Serial.println("I2S DMA buffer alloc failed"); + deinit(); + return false; } + memset(_dmaBuffer[i], 0, _bufferSize); - // Setup DMA descriptors - circular chain for gapless ping-pong - for (int i = 0; i < 2; i++) { - _dmaDesc[i]->size = _bufferSize; - _dmaDesc[i]->length = _bufferSize; - _dmaDesc[i]->buf = _dmaBuffer[i]; - _dmaDesc[i]->eof = 1; // Generate interrupt on completion - _dmaDesc[i]->sosf = 0; - _dmaDesc[i]->owner = 1; + _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + Serial.println("I2S DMA desc alloc failed"); + deinit(); + return false; } - _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; - _dmaDesc[1]->qe.stqe_next = _dmaDesc[0]; - - // Enable I2S peripheral + } + + // Setup DMA descriptors - circular chain for gapless ping-pong + for (int i = 0; i < 2; i++) { + _dmaDesc[i]->size = _bufferSize; + _dmaDesc[i]->length = _bufferSize; + _dmaDesc[i]->buf = _dmaBuffer[i]; + _dmaDesc[i]->eof = 1; // Generate interrupt on completion + _dmaDesc[i]->sosf = 0; + _dmaDesc[i]->owner = 1; + } + _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; + _dmaDesc[1]->qe.stqe_next = _dmaDesc[0]; + + // Enable I2S peripheral #if defined(WLEDPB_ESP32) - periph_module_enable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); + periph_module_enable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); #else - periph_module_enable(PERIPH_I2S0_MODULE); + 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 8-bit parallel LCD mode (matching NeoPixelBus) - _i2sDev->conf2.val = 0; - _i2sDev->conf2.lcd_en = 1; - _i2sDev->conf2.lcd_tx_wrx2_en = 1; // Required for 8-bit parallel output - _i2sDev->conf2.lcd_tx_sdx2_en = 0; - - // DMA config - _i2sDev->lc_conf.val = 0; - _i2sDev->lc_conf.out_eof_mode = 1; - - // Disable PDM + // 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 8-bit parallel LCD mode (matching NeoPixelBus) + _i2sDev->conf2.val = 0; + _i2sDev->conf2.lcd_en = 1; + _i2sDev->conf2.lcd_tx_wrx2_en = 1; // Required for 8-bit parallel output + _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(WLEDPB_ESP32) - _i2sDev->pdm_conf.tx_pdm_en = 0; - _i2sDev->pdm_conf.pcm2pdm_conv_en = 0; + _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 = 1; // 0=16bit dual, 1=16bit single, 2=32bit dual, 3=32bit single) - _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold - - // PCM bypass - _i2sDev->conf1.val = 0; - _i2sDev->conf1.tx_stop_en = 0; - _i2sDev->conf1.tx_pcm_bypass = 1; - - // Channel config - _i2sDev->conf_chan.val = 0; - _i2sDev->conf_chan.tx_chan_mod = 1; // 0=stereo, 1=right-left, 2=left-right, 3=right only, 4=left only - - // I2S conf - _i2sDev->conf.val = 0; - _i2sDev->conf.tx_msb_shift = 0; // No shift in parallel mode - _i2sDev->conf.tx_right_first = 1; - #if defined(CONFIG_IDF_TARGET_ESP32S2) - _i2sDev->conf.tx_dma_equal = 1; // seems required for S2 - #endif - - // Clear timing register - _i2sDev->timing.val = 0; - - // Calculate clock divider for 4-step cadence - // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) - // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns - // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) - const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 - uint32_t bitPeriodNs = timing.bitPeriod(); + // FIFO configuration + _i2sDev->fifo_conf.val = 0; + _i2sDev->fifo_conf.tx_fifo_mod_force_en = 1; + _i2sDev->fifo_conf.tx_fifo_mod = 1; // 0=16bit dual, 1=16bit single, 2=32bit dual, 3=32bit single) + _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold + + // PCM bypass + _i2sDev->conf1.val = 0; + _i2sDev->conf1.tx_stop_en = 0; + _i2sDev->conf1.tx_pcm_bypass = 1; + + // Channel config + _i2sDev->conf_chan.val = 0; + _i2sDev->conf_chan.tx_chan_mod = 1; // 0=stereo, 1=right-left, 2=left-right, 3=right only, 4=left only + + // I2S conf + _i2sDev->conf.val = 0; + _i2sDev->conf.tx_msb_shift = 0; // No shift in parallel mode + _i2sDev->conf.tx_right_first = 1; + #if defined(CONFIG_IDF_TARGET_ESP32S2) + _i2sDev->conf.tx_dma_equal = 1; // seems required for S2 + #endif + + // Clear timing register + _i2sDev->timing.val = 0; + + // Calculate clock divider for 4-step cadence + // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) + // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns + // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) + const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 + uint32_t bitPeriodNs = timing.bitPeriod(); #if defined(WLEDPB_ESP32) - const double baseClockMhz = 160.0; + const double baseClockMhz = 160.0; #else - const double baseClockMhz = 80.0; // S2 has 80MHz I2S base clock + const double baseClockMhz = 80.0; // S2 has 80MHz I2S base clock #endif - // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz - // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 - double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; - if (clkmdiv < 2.0) clkmdiv = 2.0; - if (clkmdiv > 255.0) clkmdiv = 255.0; - - uint8_t clkmInteger = (uint8_t)clkmdiv; - double clkmFraction = clkmdiv - clkmInteger; - - // Convert fraction to divB/divA (fraction = divB/divA) - uint8_t divB = 0; - uint8_t divA = 0; - if (clkmFraction > 0.001) { - divA = 63; // use max denominator for best precision - divB = (uint8_t)(clkmFraction * 63.0 + 0.5); - if (divB >= divA) divB = divA - 1; - } - - _clockDiv = clkmInteger; - - Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", - bitPeriodNs, clkmInteger, divB, divA, bckDiv); - double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; - if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; - Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", - actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); - - // Set clock (with fractional divider for accurate timing) - _i2sDev->clkm_conf.val = 0; - _i2sDev->clkm_conf.clk_en = 1; - _i2sDev->clkm_conf.clkm_div_a = divA; - _i2sDev->clkm_conf.clkm_div_b = divB; - _i2sDev->clkm_conf.clkm_div_num = clkmInteger; - - // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) - _i2sDev->sample_rate_conf.val = 0; - _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; - _i2sDev->sample_rate_conf.tx_bits_mod = 8; // TODO: try 16 bit mode an bump up outputs to 16 parallel channels, just like LCD driver does now - - // 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; + // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz + // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 + double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; + if (clkmdiv < 2.0) clkmdiv = 2.0; + if (clkmdiv > 255.0) clkmdiv = 255.0; + + uint8_t clkmInteger = (uint8_t)clkmdiv; + double clkmFraction = clkmdiv - clkmInteger; + + // Convert fraction to divB/divA (fraction = divB/divA) + uint8_t divB = 0; + uint8_t divA = 0; + if (clkmFraction > 0.001) { + divA = 63; // use max denominator for best precision + divB = (uint8_t)(clkmFraction * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + _clockDiv = clkmInteger; + + Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", + bitPeriodNs, clkmInteger, divB, divA, bckDiv); + double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; + if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; + Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", + actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); + + // Set clock (with fractional divider for accurate timing) + _i2sDev->clkm_conf.val = 0; + _i2sDev->clkm_conf.clk_en = 1; + _i2sDev->clkm_conf.clkm_div_a = divA; + _i2sDev->clkm_conf.clkm_div_b = divB; + _i2sDev->clkm_conf.clkm_div_num = clkmInteger; + + // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) + _i2sDev->sample_rate_conf.val = 0; + _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; + _i2sDev->sample_rate_conf.tx_bits_mod = 8; // TODO: try 16 bit mode an bump up outputs to 16 parallel channels, just like LCD driver does now + + // 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(WLEDPB_ESP32) - intSource = (_busNum == 0) ? ETS_I2S0_INTR_SOURCE : ETS_I2S1_INTR_SOURCE; + intSource = (_busNum == 0) ? ETS_I2S0_INTR_SOURCE : ETS_I2S1_INTR_SOURCE; #else - intSource = ETS_I2S0_INTR_SOURCE; + intSource = ETS_I2S0_INTR_SOURCE; #endif - esp_err_t err = esp_intr_alloc(intSource, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, - dmaISR, this, &_isrHandle); - if (err != ESP_OK) { - Serial.printf("I2S ISR alloc failed: %d", err); - deinit(); - return false; - } + esp_err_t err = esp_intr_alloc(intSource, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, + dmaISR, this, &_isrHandle); + if (err != ESP_OK) { + Serial.printf("I2S ISR alloc failed: %d", err); + deinit(); + return false; + } - _initialized = true; - Serial.printf("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); - return true; + _initialized = true; + Serial.printf("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); + return true; } void I2sBusContext::deinit() { - if (_i2sDev) { - _i2sDev->int_ena.val = 0; // Disable interrupts first - _i2sDev->conf.tx_start = 0; - _i2sDev->out_link.start = 0; + if (_i2sDev) { + _i2sDev->int_ena.val = 0; // Disable interrupts first + _i2sDev->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } + _state = DriverState::Idle; + + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; } - _state = DriverState::Idle; - - if (_isrHandle) { - esp_intr_free(_isrHandle); - _isrHandle = nullptr; - } - - for (int i = 0; i < 2; i++) { - if (_dmaBuffer[i]) { - heap_caps_free(_dmaBuffer[i]); - _dmaBuffer[i] = nullptr; - } - if (_dmaDesc[i]) { - heap_caps_free(_dmaDesc[i]); - _dmaDesc[i] = nullptr; - } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; } + } #if defined(WLEDPB_ESP32) - periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); + periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); #else - periph_module_disable(PERIPH_I2S0_MODULE); + periph_module_disable(PERIPH_I2S0_MODULE); #endif - _initialized = false; + _initialized = false; } int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { - // Find free slot - int8_t idx = -1; - for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { - if (! _channels[i].active) { - idx = i; - break; - } + // Find free slot + int8_t idx = -1; + for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { + if (! _channels[i].active) { + idx = i; + break; } + } - if (idx < 0) return -1; + if (idx < 0) return -1; - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); - // Configure GPIO - gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + // Configure GPIO + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); - // Route I2S output to GPIO - int sigIdx; + // Route I2S output to GPIO + int sigIdx; #if defined(WLEDPB_ESP32) - sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; #elif defined(WLEDPB_ESP32S2) - sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX + sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX #else - sigIdx = I2S0O_DATA_OUT0_IDX; + sigIdx = I2S0O_DATA_OUT0_IDX; #endif - sigIdx += idx; + sigIdx += idx; - gpio_matrix_out(pin, sigIdx, false, false); + gpio_matrix_out(pin, sigIdx, false, false); - return idx; + return idx; } void I2sBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; - if (_channels[channelIdx].pin >= 0) { - gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); - } + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } - _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); } void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; + if (channelIdx < 0 || channelIdx >= WLEDPB_I2S_MAX_CHANNELS) return; - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - _channels[channelIdx].srcPos = 0; + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; - if (len > _maxDataLen) { - _maxDataLen = len; - } + if (len > _maxDataLen) { + _maxDataLen = len; + } - // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() - if (_stagedMask & (1 << channelIdx)) { - _stagedMask = 0; - } - _stagedMask |= (1 << channelIdx); + // Safety: If this channel was already staged, it means we somehow missed triggering startTransmit() + if (_stagedMask & (1 << channelIdx)) { + _stagedMask = 0; + } + _stagedMask |= (1 << channelIdx); } void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { - // 4-step cadence encoding for parallel output - // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) - // Desired output: [HIGH][data][data][LOW] for each bit - // - // ESP32 I2S LCD mode with lcd_tx_wrx2_en=1 swaps half-words within 32-bit values: - // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] - // (NeoPixelBus documents this as "bytes within the words are swapped") - // So we write: p[0]=step2, p[1]=step3, p[2]=step0, p[3]=step1 - // - // Buffer is always filled completely (zeros = LOW = reset signal) - - memset(dest, 0, destLen); - size_t pos = 0; - - // Pre-calculate max channels to speed up loop - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; - } - - // Process each source byte position across all channels - while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte - bool hasData = false; - - for (int ch = 0; ch < maxCh; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - - hasData = true; - uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; - uint8_t chMask = (1 << ch); - - uint8_t* p = dest + pos; + // 4-step cadence encoding for parallel output + // Each source bit becomes 4 DMA bytes (one bit per channel in each byte) + // Desired output: [HIGH][data][data][LOW] for each bit + // + // ESP32 I2S LCD mode with lcd_tx_wrx2_en=1 swaps half-words within 32-bit values: + // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] + // (NeoPixelBus documents this as "bytes within the words are swapped") + // So we write: p[0]=step2, p[1]=step3, p[2]=step0, p[3]=step1 + // + // Buffer is always filled completely (zeros = LOW = reset signal) + + memset(dest, 0, destLen); + size_t pos = 0; + + // Pre-calculate max channels to speed up loop + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) maxCh = ch + 1; + } + + // Process each source byte position across all channels + while (pos + 32 <= destLen) { // 8 bits * 4 steps = 32 bytes per source byte + bool hasData = false; + + for (int ch = 0; ch < maxCh; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + hasData = true; + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint8_t chMask = (1 << ch); + + uint8_t* p = dest + pos; #if defined(WLEDPB_ESP32S2) - // ESP32-S2 does NOT swap half-words (memory layout [step0, step1, step2, step3]) - // bit 7 - p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 6 - p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 5 - p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 4 - p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 3 - p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 2 - p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 1 - p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 0 - p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // ESP32-S2 does NOT swap half-words (memory layout [step0, step1, step2, step3]) + // bit 7 + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 6 + p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 5 + p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 4 + p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 3 + p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 2 + p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 1 + p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 0 + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; #else - // Half-word swapped: memory layout [step2, step3, step0, step1] - // bit 7 - p[2] |= chMask; if (srcByte & 0x80) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 6 - p[2] |= chMask; if (srcByte & 0x40) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 5 - p[2] |= chMask; if (srcByte & 0x20) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 4 - p[2] |= chMask; if (srcByte & 0x10) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 3 - p[2] |= chMask; if (srcByte & 0x08) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 2 - p[2] |= chMask; if (srcByte & 0x04) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 1 - p[2] |= chMask; if (srcByte & 0x02) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 0 - p[2] |= chMask; if (srcByte & 0x01) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // Half-word swapped: memory layout [step2, step3, step0, step1] + // bit 7 + p[2] |= chMask; if (srcByte & 0x80) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 6 + p[2] |= chMask; if (srcByte & 0x40) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 5 + p[2] |= chMask; if (srcByte & 0x20) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 4 + p[2] |= chMask; if (srcByte & 0x10) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 3 + p[2] |= chMask; if (srcByte & 0x08) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 2 + p[2] |= chMask; if (srcByte & 0x04) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 1 + p[2] |= chMask; if (srcByte & 0x02) { p[3] |= chMask; p[0] |= chMask; } p += 4; + // bit 0 + p[2] |= chMask; if (srcByte & 0x01) { p[3] |= chMask; p[0] |= chMask; } p += 4; #endif - } - - if (!hasData) break; + } - // Advance all channel positions - for (int ch = 0; ch < maxCh; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - _channels[ch].srcPos++; - } - } + if (!hasData) break; - pos += 32; + // Advance all channel positions + for (int ch = 0; ch < maxCh; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } } - // Rest of buffer remains zero (reset signal) from memset + + pos += 32; + } + // Rest of buffer remains zero (reset signal) from memset } void I2sBusContext::fillBuffer(uint8_t bufIdx) { - encode4Step(_dmaBuffer[bufIdx], _bufferSize); - // desc->length stays at _bufferSize (set in init, never changes) + encode4Step(_dmaBuffer[bufIdx], _bufferSize); + // desc->length stays at _bufferSize (set in init, never changes) } // ----- I2S ISR Tracking ----- @@ -462,259 +462,259 @@ static volatile uint32_t s_i2sIsrReset = 0; static volatile uint32_t s_i2sIsrIdle = 0; bool I2sBusContext::startTransmit() { - if (_state != DriverState::Idle) return false; - if (_channelCount == 0) return false; + 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 false; + _stagedMask = 0; // Reset for next frame + + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } + } + } + + // Fill both buffers initially + fillBuffer(0); + fillBuffer(1); + + // Restore DMA descriptor ownership (DMA clears owner to 0 after processing) + _dmaDesc[0]->owner = 1; + _dmaDesc[1]->owner = 1; + + _activeBuffer = 0; + _state = DriverState::Sending; + + // Reset DMA and FIFO before starting + _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; + + // Clear and enable interrupts + _i2sDev->int_clr.val = 0xFFFFFFFF; + _i2sDev->int_ena.out_eof = 1; + + // Enable DMA and start + _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; + + // ----- DEBUG BLOCK START ----- + /* + static uint32_t last_isr = 0; + uint32_t diff_isr = s_i2sIsrCount - last_isr; + last_isr = s_i2sIsrCount; + Serial.printf("[I2S-Tx] startTransmit triggering. ISR count delta since last tx: %u\n", diff_isr); + Serial.printf("[I2S-Tx] State vars: isrTotal=%u, send=%u, reset=%u, idle=%u\n", + s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle); + Serial.printf("[I2S-Tx] HW Regs: conf(0x%08x) conf1(0x%08x) conf2(0x%08x)\n", + _i2sDev->conf.val, _i2sDev->conf1.val, _i2sDev->conf2.val); + Serial.printf("[I2S-Tx] int_ena(0x%08x) int_raw(0x%08x)\n", + _i2sDev->int_ena.val, _i2sDev->int_raw.val); + Serial.printf("[I2S-Tx] out_link(0x%08x) lc_conf(0x%08x)\n", + _i2sDev->out_link.val, _i2sDev->lc_conf.val); + */ + // ----- DEBUG BLOCK END ----- + + return true; +} - // Only start transmission if ALL active channels have populated data - if (_stagedMask != _channelMask) return false; - _stagedMask = 0; // Reset for next frame +void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { + I2sBusContext* ctx = (I2sBusContext*)arg; + i2s_dev_t* dev = ctx->_i2sDev; - _maxDataLen = 0; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active) { - _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) { - _maxDataLen = _channels[ch].srcLen; - } - } - } + uint32_t status = dev->int_st.val; + dev->int_clr.val = status; - // Fill both buffers initially - fillBuffer(0); - fillBuffer(1); - - // Restore DMA descriptor ownership (DMA clears owner to 0 after processing) - _dmaDesc[0]->owner = 1; - _dmaDesc[1]->owner = 1; - - _activeBuffer = 0; - _state = DriverState::Sending; - - // Reset DMA and FIFO before starting - _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; - - // Clear and enable interrupts - _i2sDev->int_clr.val = 0xFFFFFFFF; - _i2sDev->int_ena.out_eof = 1; - - // Enable DMA and start - _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; - - // ----- DEBUG BLOCK START ----- - /* - static uint32_t last_isr = 0; - uint32_t diff_isr = s_i2sIsrCount - last_isr; - last_isr = s_i2sIsrCount; - Serial.printf("[I2S-Tx] startTransmit triggering. ISR count delta since last tx: %u\n", diff_isr); - Serial.printf("[I2S-Tx] State vars: isrTotal=%u, send=%u, reset=%u, idle=%u\n", - s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle); - Serial.printf("[I2S-Tx] HW Regs: conf(0x%08x) conf1(0x%08x) conf2(0x%08x)\n", - _i2sDev->conf.val, _i2sDev->conf1.val, _i2sDev->conf2.val); - Serial.printf("[I2S-Tx] int_ena(0x%08x) int_raw(0x%08x)\n", - _i2sDev->int_ena.val, _i2sDev->int_raw.val); - Serial.printf("[I2S-Tx] out_link(0x%08x) lc_conf(0x%08x)\n", - _i2sDev->out_link.val, _i2sDev->lc_conf.val); - */ - // ----- DEBUG BLOCK END ----- + if (!(status & I2S_OUT_EOF_INT_ST)) return; - return true; -} + s_i2sIsrCount++; // debug, remove -void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { - I2sBusContext* ctx = (I2sBusContext*)arg; - i2s_dev_t* dev = ctx->_i2sDev; - - uint32_t status = dev->int_st.val; - dev->int_clr.val = status; - - if (!(status & I2S_OUT_EOF_INT_ST)) return; - - s_i2sIsrCount++; // debug, remove - - // The completed buffer just finished playing; DMA is now on the other buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer ^= 1; - - if (ctx->_state == DriverState::Sending) { - s_i2sIsrSending++; // debug, remove - // Encode next chunk into the completed buffer - // encode4Step always fills the full buffer (zeros for any remainder) - ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); - - // Check if all source data has been consumed - bool moreData = false; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && - ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { - moreData = true; - break; - } - } - if (!moreData) { - // Last data chunk was just encoded into completedBuf. - // DMA is currently playing the OTHER buffer. We need to wait - // for that to finish so the last-data buffer gets played. - ctx->_state = DriverState::SendingLast; - } - - // Restore DMA ownership so hardware can replay this buffer - ctx->_dmaDesc[completedBuf]->owner = 1; - } else if (ctx->_state == DriverState::SendingLast) { - // The other buffer just finished. DMA is now playing the last-data buffer. - // Fill completed buffer with zeros (reset signal) so it plays after. - memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); - ctx->_dmaDesc[completedBuf]->owner = 1; - s_i2sIsrReset++; // debug, remove - ctx->_state = DriverState::WaitingReset; - } else { - // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. - s_i2sIsrIdle++; // debug, remove - dev->int_ena.out_eof = 0; - dev->conf.tx_start = 0; - dev->out_link.start = 0; - ctx->_state = DriverState::Idle; + // The completed buffer just finished playing; DMA is now on the other buffer + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; + + if (ctx->_state == DriverState::Sending) { + s_i2sIsrSending++; // debug, remove + // Encode next chunk into the completed buffer + // encode4Step always fills the full buffer (zeros for any remainder) + ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); + + // Check if all source data has been consumed + bool moreData = false; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (ctx->_channels[ch].active && + ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { + moreData = true; + break; + } } + if (!moreData) { + // Last data chunk was just encoded into completedBuf. + // DMA is currently playing the OTHER buffer. We need to wait + // for that to finish so the last-data buffer gets played. + ctx->_state = DriverState::SendingLast; + } + + // Restore DMA ownership so hardware can replay this buffer + ctx->_dmaDesc[completedBuf]->owner = 1; + } else if (ctx->_state == DriverState::SendingLast) { + // The other buffer just finished. DMA is now playing the last-data buffer. + // Fill completed buffer with zeros (reset signal) so it plays after. + memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); + ctx->_dmaDesc[completedBuf]->owner = 1; + s_i2sIsrReset++; // debug, remove + ctx->_state = DriverState::WaitingReset; + } else { + // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. + s_i2sIsrIdle++; // debug, remove + dev->int_ena.out_eof = 0; + dev->conf.tx_start = 0; + dev->out_link.start = 0; + ctx->_state = DriverState::Idle; + } } // I2sBus implementation I2sBus::I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, - uint8_t busNum, size_t bufferSize) - : _pin(pin) - , _busNum(busNum) - , _bufferSize(bufferSize) - , _timing(timing) - , _order(order) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) + uint8_t busNum, size_t bufferSize) + : _pin(pin) + , _busNum(busNum) + , _bufferSize(bufferSize) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) { } I2sBus::~I2sBus() { - end(); + end(); } bool I2sBus::begin() { - if (_initialized) return true; - - _ctx = I2sBusContext::get(_busNum); - if (!_ctx) return false; - - if (!_ctx->init(_timing, _bufferSize)) { - I2sBusContext::release(_busNum); - _ctx = nullptr; - return false; - } - - _channelIdx = _ctx->registerChannel(_pin, this); - if (_channelIdx < 0) { - Serial.printf("[I2S] registerChannel failed for pin %d\n", _pin); - I2sBusContext::release(_busNum); - _ctx = nullptr; - return false; - } - - _initialized = true; - Serial.printf("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n", _pin, _busNum, _channelIdx); - return true; + if (_initialized) return true; + + _ctx = I2sBusContext::get(_busNum); + if (!_ctx) return false; + + if (!_ctx->init(_timing, _bufferSize)) { + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + Serial.printf("[I2S] registerChannel failed for pin %d\n", _pin); + I2sBusContext::release(_busNum); + _ctx = nullptr; + return false; + } + + _initialized = true; + Serial.printf("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n", _pin, _busNum, _channelIdx); + return true; } void I2sBus::end() { - if (!_initialized) return; - - if (_ctx) { - // Wait for any active transmission to complete before cleanup - while (!_ctx->isIdle()) vTaskDelay(1); - _ctx->unregisterChannel(_channelIdx); - I2sBusContext::release(_busNum); - _ctx = nullptr; - } - - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } - - _initialized = false; + if (!_initialized) return; + + if (_ctx) { + // Wait for any active transmission to complete before cleanup + while (!_ctx->isIdle()) vTaskDelay(1); + _ctx->unregisterChannel(_channelIdx); + I2sBusContext::release(_busNum); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } + + _initialized = false; } bool I2sBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) { - return true; - } - - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - } - - _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { return true; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + } + + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_DMA); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; } bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - // Always use internal pixel buffer (WLED always calls show() without args) - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + // Always use internal pixel buffer (WLED always calls show() without args) + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - // Wait for previous transmission to complete - while (!_ctx->isIdle()) { - vTaskDelay(1); - } + // Wait for previous transmission to complete + while (!_ctx->isIdle()) { + vTaskDelay(1); + } - if (!allocateBuffer(_numPixels)) return false; + if (!allocateBuffer(_numPixels)) return false; - // Encode pixels to byte stream - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); - for (uint16_t i = 0; i < _numPixels; i++) { - encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); - dst += numCh; - } + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } - // Set data for our channel and start - _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); - return _ctx->startTransmit(); + // Set data for our channel and start + _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); + return _ctx->startTransmit(); } bool I2sBus::canShow() const { - if (!_ctx) return true; - return _ctx->isIdle(); + if (!_ctx) return true; + return _ctx->isIdle(); } void I2sBus::waitComplete() { - while (_ctx && !_ctx->isIdle()) { - vTaskDelay(1); - } + while (_ctx && !_ctx->isIdle()) { + vTaskDelay(1); + } } void I2sBus::setColorOrder(ColorOrder order) { - _order = order; + _order = order; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index d627e65385..faf705c4af 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -15,11 +15,11 @@ namespace WLEDpixelBus { #include "esp_intr_alloc.h" #if defined(WLEDPB_ESP32) - #define WLEDPB_I2S_BUS_COUNT 2 // TODO: support both buses on ESP32? (currently only bus 1 is used for LED output, one is for AR) - #define WLEDPB_I2S_MAX_CHANNELS 16 + #define WLEDPB_I2S_BUS_COUNT 2 // TODO: support both buses on ESP32? (currently only bus 1 is used for LED output, one is for AR) + #define WLEDPB_I2S_MAX_CHANNELS 16 #else - #define WLEDPB_I2S_BUS_COUNT 1 - #define WLEDPB_I2S_MAX_CHANNELS 8 + #define WLEDPB_I2S_BUS_COUNT 1 + #define WLEDPB_I2S_MAX_CHANNELS 8 #endif /** @@ -28,70 +28,70 @@ namespace WLEDpixelBus { */ class I2sBusContext { public: - static I2sBusContext* get(uint8_t busNum); - static void release(uint8_t busNum); + static I2sBusContext* get(uint8_t busNum); + static void release(uint8_t busNum); - bool init(const LedTiming& timing, size_t bufferSize); - void deinit(); + bool init(const LedTiming& timing, size_t bufferSize); + void deinit(); - // Channel management - int8_t registerChannel(int8_t pin, I2sBus* bus); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } + // Channel management + int8_t registerChannel(int8_t pin, I2sBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } - // Transmission - bool startTransmit(); - bool isIdle() const { return _state == DriverState::Idle; } + // Transmission + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } - // Data access for channels - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + // Data access for channels + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); private: - I2sBusContext(uint8_t busNum); - ~I2sBusContext(); - - void fillBuffer(uint8_t bufIdx); - static void IRAM_ATTR dmaISR(void* arg); - - uint8_t _busNum; - i2s_dev_t* _i2sDev; - volatile DriverState _state; - bool _initialized; - - // DMA double buffer - lldesc_t* _dmaDesc[2]; - uint8_t* _dmaBuffer[2]; - size_t _bufferSize; - volatile uint8_t _activeBuffer; - - // Timing - LedTiming _timing; - uint32_t _clockDiv; - - // ISR handle - intr_handle_t _isrHandle; - - // Channel data - struct ChannelData { - I2sBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - size_t srcPos; - bool active; - }; - ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; - uint8_t _channelCount; - uint16_t _channelMask; - uint16_t _stagedMask; - size_t _maxDataLen; - - // Encoding (4-step cadence) - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); - - // Singleton instances - static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; - static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; + I2sBusContext(uint8_t busNum); + ~I2sBusContext(); + + void fillBuffer(uint8_t bufIdx); + static void IRAM_ATTR dmaISR(void* arg); + + uint8_t _busNum; + i2s_dev_t* _i2sDev; + volatile DriverState _state; + bool _initialized; + + // DMA double buffer + lldesc_t* _dmaDesc[2]; + uint8_t* _dmaBuffer[2]; + size_t _bufferSize; + volatile uint8_t _activeBuffer; + + // Timing + LedTiming _timing; + uint32_t _clockDiv; + + // ISR handle + intr_handle_t _isrHandle; + + // Channel data + struct ChannelData { + I2sBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_I2S_MAX_CHANNELS]; + uint8_t _channelCount; + uint16_t _channelMask; + uint16_t _stagedMask; + size_t _maxDataLen; + + // Encoding (4-step cadence) + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + + // Singleton instances + static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; + static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; }; /** @@ -99,45 +99,45 @@ class I2sBusContext { */ class I2sBus : public IBus { public: - /** - * Create I2S bus - * @param pin GPIO pin - * @param timing LED timing - * @param order Color order - * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) - * @param bufferSize DMA buffer size - */ - I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, - uint8_t busNum = 1, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); - ~I2sBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "I2S"; } - - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order); + /** + * Create I2S bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) + * @param bufferSize DMA buffer size + */ + I2sBus(int8_t pin, const LedTiming& timing, ColorOrder order, + uint8_t busNum = 1, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); + ~I2sBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "I2S"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); private: - int8_t _pin; - uint8_t _busNum; - size_t _bufferSize; - LedTiming _timing; - ColorOrder _order; - bool _initialized; + int8_t _pin; + uint8_t _busNum; + size_t _bufferSize; + LedTiming _timing; + ColorOrder _order; + bool _initialized; - int8_t _channelIdx; - I2sBusContext* _ctx; + int8_t _channelIdx; + I2sBusContext* _ctx; - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; - bool allocateBuffer(uint16_t numPixels); + bool allocateBuffer(uint16_t numPixels); }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 7f6299e549..a694132424 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -35,423 +35,423 @@ LcdBusContext* LcdBusContext::_instance = nullptr; uint8_t LcdBusContext::_refCount = 0; LcdBusContext* LcdBusContext::get() { - if (_instance == nullptr) { - _instance = new LcdBusContext(); - } - _refCount++; - return _instance; + if (_instance == nullptr) { + _instance = new LcdBusContext(); + } + _refCount++; + return _instance; } void LcdBusContext::release() { - if (_refCount == 0) return; - _refCount--; - if (_refCount == 0 && _instance) { - delete _instance; - _instance = nullptr; - } + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } } LcdBusContext::LcdBusContext() - : _state(DriverState::Idle) - , _initialized(false) - , _use16Bit(false) - , _dmaChannel(nullptr) - , _timing{0, 0, 0, 0, 0} - , _bufferSize(WLEDPB_LCD_DMA_BUFFER_SIZE) - , _channelCount(0) - , _channelMask(0) - , _stagedMask(0) - , _maxDataLen(0) - , _activeBuffer(0) + : _state(DriverState::Idle) + , _initialized(false) + , _use16Bit(false) + , _dmaChannel(nullptr) + , _timing{0, 0, 0, 0, 0} + , _bufferSize(WLEDPB_LCD_DMA_BUFFER_SIZE) + , _channelCount(0) + , _channelMask(0) + , _stagedMask(0) + , _maxDataLen(0) + , _activeBuffer(0) { - for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; - } - for (int i = 0; i < 2; i++) { - _dmaDesc[i] = nullptr; - _dmaBuffer[i] = nullptr; - } + for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; + } + for (int i = 0; i < 2; i++) { + _dmaDesc[i] = nullptr; + _dmaBuffer[i] = nullptr; + } } LcdBusContext::~LcdBusContext() { - deinit(); + deinit(); } bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16Bit) { - if (_initialized) return true; - - LCD_LOG("Init: buf=%u x2, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", - WLEDPB_LCD_DMA_BUFFER_SIZE, WLEDPB_LCD_CADENCE_STEPS, - timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); - - _timing = timing; - _use16Bit = use16Bit; - _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; - - uint32_t bitPeriodNs = timing.bitPeriod(); - - // Allocate double DMA buffers - for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - LCD_LOG("ERROR: DMA buffer %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); - - _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - LCD_LOG("ERROR: DMA desc %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); - } - - // Setup DMA descriptors in CIRCULAR mode (never changes during operation!) - // buf0 -> buf1 -> buf0 -> buf1 -> ... - for (int i = 0; i < 2; i++) { - _dmaDesc[i]->dw0.size = WLEDPB_LCD_DMA_BUFFER_SIZE; - _dmaDesc[i]->dw0.length = WLEDPB_LCD_DMA_BUFFER_SIZE; - _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR - _dmaDesc[i]->buffer = _dmaBuffer[i]; - _dmaDesc[i]->next = _dmaDesc[i ^ 1]; // Point to other buffer (circular) - } - - - - // Enable LCD_CAM peripheral - periph_module_enable(PERIPH_LCD_CAM_MODULE); - periph_module_reset(PERIPH_LCD_CAM_MODULE); + if (_initialized) return true; - // 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); + LCD_LOG("Init: buf=%u x2, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", + WLEDPB_LCD_DMA_BUFFER_SIZE, WLEDPB_LCD_CADENCE_STEPS, + timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); - // Calculate clock divider - double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; - - LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); + _timing = timing; + _use16Bit = use16Bit; + _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; - if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { - LCD_LOG("ERROR: Invalid clock divider"); - deinit(); - 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; - } + uint32_t bitPeriodNs = timing.bitPeriod(); - // 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; - LCD_CAM.lcd_user.lcd_2byte_en = 1; // Always use 16-bit output for 16 channels - 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) { - LCD_LOG("ERROR: GDMA alloc failed: %d", err); - deinit(); - return false; + // Allocate double DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + LCD_LOG("ERROR: DMA buffer %d alloc failed", i); + deinit(); + return false; } + memset(_dmaBuffer[i], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); - err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); - if (err != ESP_OK) { - LCD_LOG("ERROR: GDMA connect failed: %d", err); - deinit(); - return false; + _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + LCD_LOG("ERROR: DMA desc %d alloc failed", i); + deinit(); + return false; } + memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); + } + + // Setup DMA descriptors in CIRCULAR mode (never changes during operation!) + // buf0 -> buf1 -> buf0 -> buf1 -> ... + for (int i = 0; i < 2; i++) { + _dmaDesc[i]->dw0.size = WLEDPB_LCD_DMA_BUFFER_SIZE; + _dmaDesc[i]->dw0.length = WLEDPB_LCD_DMA_BUFFER_SIZE; + _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR + _dmaDesc[i]->buffer = _dmaBuffer[i]; + _dmaDesc[i]->next = _dmaDesc[i ^ 1]; // Point to other buffer (circular) + } + + + + // 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 + double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; + + LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); + + if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { + LCD_LOG("ERROR: Invalid clock divider"); + deinit(); + 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; + LCD_CAM.lcd_user.lcd_2byte_en = 1; // Always use 16-bit output for 16 channels + 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) { + LCD_LOG("ERROR: GDMA alloc failed: %d", err); + deinit(); + return false; + } - gdma_strategy_config_t strategy_config = { - .owner_check = false, - .auto_update_desc = false - }; - gdma_apply_strategy(_dmaChannel, &strategy_config); - - // Register DMA callback - gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; - gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); - - _initialized = true; - LCD_LOG("Init OK: clkm_div=%u+%u/%u", - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_num, - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_b, - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_a); - return true; + err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + if (err != ESP_OK) { + LCD_LOG("ERROR: GDMA connect failed: %d", err); + deinit(); + return false; + } + + gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(_dmaChannel, &strategy_config); + + // Register DMA callback + gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; + gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); + + _initialized = true; + LCD_LOG("Init OK: clkm_div=%u+%u/%u", + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_num, + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_b, + (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_a); + return true; } void LcdBusContext::deinit() { - // Stop transmission - LCD_CAM.lcd_user.lcd_start = 0; - _state = DriverState::Idle; - - if (_dmaChannel) { - gdma_stop(_dmaChannel); - gdma_disconnect(_dmaChannel); - gdma_del_channel(_dmaChannel); - _dmaChannel = nullptr; + // Stop transmission + LCD_CAM.lcd_user.lcd_start = 0; + _state = DriverState::Idle; + + if (_dmaChannel) { + gdma_stop(_dmaChannel); + gdma_disconnect(_dmaChannel); + gdma_del_channel(_dmaChannel); + _dmaChannel = nullptr; + } + + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; } - - for (int i = 0; i < 2; i++) { - if (_dmaBuffer[i]) { - heap_caps_free(_dmaBuffer[i]); - _dmaBuffer[i] = nullptr; - } - if (_dmaDesc[i]) { - heap_caps_free(_dmaDesc[i]); - _dmaDesc[i] = nullptr; - } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; } + } - if (_initialized) { - periph_module_disable(PERIPH_LCD_CAM_MODULE); - } - - _initialized = false; + if (_initialized) { + periph_module_disable(PERIPH_LCD_CAM_MODULE); + } + + _initialized = false; } int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus) { - int8_t idx = -1; - for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - if (!_channels[i].active) { - idx = i; - break; - } + int8_t idx = -1; + for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; } - if (idx < 0) return -1; + } + if (idx < 0) return -1; - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); - esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, false, false); - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); - gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); + esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, false, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); - LCD_LOG("Channel %d: pin=%d, mask=0x%04X", idx, pin, _channelMask); - return idx; + LCD_LOG("Channel %d: pin=%d, mask=0x%04X", idx, pin, _channelMask); + return idx; } void LcdBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; + if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; - if (_channels[channelIdx].pin >= 0) { - gpio_matrix_out(_channels[channelIdx].pin, SIG_GPIO_OUT_IDX, false, false); - pinMode(_channels[channelIdx].pin, INPUT); - } + if (_channels[channelIdx].pin >= 0) { + gpio_matrix_out(_channels[channelIdx].pin, SIG_GPIO_OUT_IDX, false, false); + pinMode(_channels[channelIdx].pin, INPUT); + } - _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); + _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); } void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; + if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - _channels[channelIdx].srcPos = 0; + _channels[channelIdx].srcData = data; + _channels[channelIdx].srcLen = len; + _channels[channelIdx].srcPos = 0; - if (len > _maxDataLen) _maxDataLen = len; + if (len > _maxDataLen) _maxDataLen = len; - if (_stagedMask & (1 << channelIdx)) _stagedMask = 0; - _stagedMask |= (1 << channelIdx); + if (_stagedMask & (1 << channelIdx)) _stagedMask = 0; + _stagedMask |= (1 << channelIdx); } void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { - // 4-step cadence encoding for parallel output without byte swapping - // Each source bit becomes 4 DMA words (one bit per channel in each 16-bit word) - // Desired output: [HIGH][data][data][LOW] for each bit - // Buffer is always filled completely (zeros = LOW = reset signal) + // 4-step cadence encoding for parallel output without byte swapping + // Each source bit becomes 4 DMA words (one bit per channel in each 16-bit word) + // Desired output: [HIGH][data][data][LOW] for each bit + // Buffer is always filled completely (zeros = LOW = reset signal) + + memset(dest, 0, destLen); + size_t pos = 0; + + // Pre-calculate max channels to speed up loop + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) maxCh = ch + 1; + } + + // Process each source byte position across all channels + while (pos + 64 <= destLen) { // 8 bits * 4 steps * 2 bytes = 64 bytes per source byte + bool hasData = false; + + for (int ch = 0; ch < maxCh; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + hasData = true; + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint16_t chMask = (1 << ch); + + uint16_t* p = (uint16_t*)(dest + pos); + + // Unrolled loop for 8 bits + // bit 7 + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 6 + p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 5 + p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 4 + p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 3 + p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 2 + p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 1 + p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; + // bit 0 + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; + } - memset(dest, 0, destLen); - size_t pos = 0; + if (!hasData) break; - // Pre-calculate max channels to speed up loop - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; + // Advance all channel positions + for (int ch = 0; ch < maxCh; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } } - // Process each source byte position across all channels - while (pos + 64 <= destLen) { // 8 bits * 4 steps * 2 bytes = 64 bytes per source byte - bool hasData = false; - - for (int ch = 0; ch < maxCh; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - - hasData = true; - uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; - uint16_t chMask = (1 << ch); - - uint16_t* p = (uint16_t*)(dest + pos); - - // Unrolled loop for 8 bits - // bit 7 - p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 6 - p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 5 - p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 4 - p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 3 - p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 2 - p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 1 - p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 0 - p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; - } - - if (!hasData) break; - - // Advance all channel positions - for (int ch = 0; ch < maxCh; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - _channels[ch].srcPos++; - } - } - - pos += 64; - } - // Rest of buffer remains zero (reset signal) from memset + pos += 64; + } + // Rest of buffer remains zero (reset signal) from memset } void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { - encode4Step(_dmaBuffer[bufIdx], _bufferSize); + encode4Step(_dmaBuffer[bufIdx], _bufferSize); } bool LcdBusContext::startTransmit() { - if (_stagedMask != _channelMask) return false; // wait for all channels - _stagedMask = 0; - - if (_state != DriverState::Idle) return false; - if (_channelCount == 0) return false; - - // Reset channel positions - _maxDataLen = 0; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) { - _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) _maxDataLen = _channels[ch].srcLen; - } + if (_stagedMask != _channelMask) return false; // wait for all channels + _stagedMask = 0; + + if (_state != DriverState::Idle) return false; + if (_channelCount == 0) return false; + + // Reset channel positions + _maxDataLen = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + _channels[ch].srcPos = 0; + if (_channels[ch].srcLen > _maxDataLen) _maxDataLen = _channels[ch].srcLen; } - if (_maxDataLen == 0) return false; + } + if (_maxDataLen == 0) return false; - // Fill both buffers initially - fillBuffer(0); - fillBuffer(1); + // Fill both buffers initially + fillBuffer(0); + fillBuffer(1); - _dmaDesc[0]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _dmaDesc[1]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[0]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[1]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _activeBuffer = 0; - _state = DriverState::Sending; + _activeBuffer = 0; + _state = DriverState::Sending; - // Start DMA (circular mode - descriptors already linked) - gdma_reset(_dmaChannel); + // Start DMA (circular mode - descriptors already linked) + 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; + 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; + gdma_start(_dmaChannel, (intptr_t)_dmaDesc[0]); + esp_rom_delay_us(1); + LCD_CAM.lcd_user.lcd_start = 1; - return true; + return true; } IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, - gdma_event_data_t* event_data, - void* user_data) { - LcdBusContext* ctx = (LcdBusContext*)user_data; - - // The completed buffer just finished playing; DMA is now on the other buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer ^= 1; - - if (ctx->_state == DriverState::Sending) { - // Encode next chunk into the completed buffer - ctx->fillBuffer(completedBuf); - - // Check if all source data has been consumed - bool moreData = false; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && - ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { - moreData = true; - break; - } - } - - if (!moreData) { - ctx->_state = DriverState::SendingLast; - } - - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - - } else if (ctx->_state == DriverState::SendingLast) { - // Fill completed buffer with zeros (reset signal) - memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - ctx->_state = DriverState::WaitingReset; - - } else { - // WaitingReset complete - stop DMA - LCD_CAM.lcd_user.lcd_start = 0; - gdma_stop(ctx->_dmaChannel); - ctx->_state = DriverState::Idle; + gdma_event_data_t* event_data, + void* user_data) { + LcdBusContext* ctx = (LcdBusContext*)user_data; + + // The completed buffer just finished playing; DMA is now on the other buffer + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer ^= 1; + + if (ctx->_state == DriverState::Sending) { + // Encode next chunk into the completed buffer + ctx->fillBuffer(completedBuf); + + // Check if all source data has been consumed + bool moreData = false; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (ctx->_channels[ch].active && + ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { + moreData = true; + break; + } } - return false; // Do not yield OS for this DMA streaming interrupt + if (!moreData) { + ctx->_state = DriverState::SendingLast; + } + + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + + } else if (ctx->_state == DriverState::SendingLast) { + // Fill completed buffer with zeros (reset signal) + memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + ctx->_state = DriverState::WaitingReset; + + } else { + // WaitingReset complete - stop DMA + LCD_CAM.lcd_user.lcd_start = 0; + gdma_stop(ctx->_dmaChannel); + ctx->_state = DriverState::Idle; + } + + return false; // Do not yield OS for this DMA streaming interrupt } void LcdBusContext::printDebugStats() { - LCD_LOG("state=%u, channels=%u, mask=0x%04X", (unsigned)_state, _channelCount, _channelMask); + LCD_LOG("state=%u, channels=%u, mask=0x%04X", (unsigned)_state, _channelCount, _channelMask); } // ============================================ @@ -459,135 +459,135 @@ void LcdBusContext::printDebugStats() { // ============================================ LcdBus::LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, - size_t bufferSize, bool use16Bit) - : _pin(pin) - , _bufferSize(bufferSize) - , _use16Bit(use16Bit) - , _timing(timing) - , _order(order) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) - , _encodedLen(0) + size_t bufferSize, bool use16Bit) + : _pin(pin) + , _bufferSize(bufferSize) + , _use16Bit(use16Bit) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) + , _encodedLen(0) { } LcdBus::~LcdBus() { - end(); + end(); } bool LcdBus::begin() { - if (_initialized) return true; - - _ctx = LcdBusContext::get(); - if (!_ctx) return false; - - if (!_ctx->init(_timing, _bufferSize, _use16Bit)) { - LcdBusContext::release(); - _ctx = nullptr; - return false; - } - - _channelIdx = _ctx->registerChannel(_pin, this); - if (_channelIdx < 0) { - LcdBusContext::release(); - _ctx = nullptr; - return false; - } - - _initialized = true; - LCD_LOG("LcdBus: ch=%d pin=%d", _channelIdx, _pin); - return true; + if (_initialized) return true; + + _ctx = LcdBusContext::get(); + if (!_ctx) return false; + + if (!_ctx->init(_timing, _bufferSize, _use16Bit)) { + LcdBusContext::release(); + _ctx = nullptr; + return false; + } + + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + LcdBusContext::release(); + _ctx = nullptr; + return false; + } + + _initialized = true; + LCD_LOG("LcdBus: ch=%d pin=%d", _channelIdx, _pin); + return true; } void LcdBus::end() { - if (!_initialized) return; + if (!_initialized) return; - if (_ctx) { - _ctx->unregisterChannel(_channelIdx); - LcdBusContext::release(); - _ctx = nullptr; - } + if (_ctx) { + _ctx->unregisterChannel(_channelIdx); + LcdBusContext::release(); + _ctx = nullptr; + } - if (_encodeBuffer) { - free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } - _initialized = false; + _initialized = false; } bool LcdBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) { - return true; - } + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { + return true; + } - if (_encodeBuffer) free(_encodeBuffer); + if (_encodeBuffer) free(_encodeBuffer); - _encodeBuffer = (uint8_t*)malloc(needed); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; - return true; + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; } bool LcdBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - // Always use internal pixel buffer (WLED always calls show() without args) - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - - // Wait for previous transmission to complete - uint32_t start = millis(); - while (!_ctx->isIdle()) { - if (millis() - start > 1000) { - _ctx->forceIdle(); - break; - } - yield(); + // Always use internal pixel buffer (WLED always calls show() without args) + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + + // Wait for previous transmission to complete + uint32_t start = millis(); + while (!_ctx->isIdle()) { + if (millis() - start > 1000) { + _ctx->forceIdle(); + break; } + yield(); + } - if (!allocateBuffer(_numPixels)) return false; + if (!allocateBuffer(_numPixels)) return false; - // Encode pixels to byte stream - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); - for (uint16_t i = 0; i < _numPixels; i++) { - encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); - dst += numCh; - } - _encodedLen = _numPixels * numCh; + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } + _encodedLen = _numPixels * numCh; - // Set data for our channel and start transmission - _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); - return _ctx->startTransmit(); + // Set data for our channel and start transmission + _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodedLen); + return _ctx->startTransmit(); } bool LcdBus::canShow() const { - if (!_ctx) return true; - return _ctx->isIdle(); + if (!_ctx) return true; + return _ctx->isIdle(); } void LcdBus::waitComplete() { - if (!_ctx) return; - uint32_t start = millis(); - while (!_ctx->isIdle()) { - if (millis() - start > 1000) { - _ctx->forceIdle(); - return; - } - yield(); + if (!_ctx) return; + uint32_t start = millis(); + while (!_ctx->isIdle()) { + if (millis() - start > 1000) { + _ctx->forceIdle(); + return; } + yield(); + } } void LcdBus::setColorOrder(ColorOrder order) { - _order = order; + _order = order; } } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index a382e94f5c..3dcddae6bd 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -26,9 +26,9 @@ namespace WLEDpixelBus { #endif #if WLEDPB_LCD_DEBUG - #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) + #define LCD_LOG(fmt, ...) Serial.printf("[LCD] " fmt "\n", ##__VA_ARGS__) #else - #define LCD_LOG(fmt, ...) + #define LCD_LOG(fmt, ...) #endif static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); @@ -40,108 +40,108 @@ static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "C class LcdBusContext { public: - static LcdBusContext* get(); - static void release(); + static LcdBusContext* get(); + static void release(); - bool init(const LedTiming& timing, size_t bufferSize, bool use16Bit = false); - void deinit(); + bool init(const LedTiming& timing, size_t bufferSize, bool use16Bit = false); + void deinit(); - int8_t registerChannel(int8_t pin, LcdBus* bus); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } + int8_t registerChannel(int8_t pin, LcdBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } - bool startTransmit(); - bool isIdle() const { return _state == DriverState::Idle; } - - void forceIdle() { - LCD_CAM.lcd_user.lcd_start = 0; - if (_dmaChannel) gdma_stop(_dmaChannel); - _state = DriverState::Idle; - } + bool startTransmit(); + bool isIdle() const { return _state == DriverState::Idle; } + + void forceIdle() { + LCD_CAM.lcd_user.lcd_start = 0; + if (_dmaChannel) gdma_stop(_dmaChannel); + _state = DriverState::Idle; + } - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); - void printDebugStats(); + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + void printDebugStats(); private: - LcdBusContext(); - ~LcdBusContext(); - - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); - void fillBuffer(uint8_t bufIdx); - - static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, - gdma_event_data_t* event_data, - void* user_data); - - volatile DriverState _state; - bool _initialized; - bool _use16Bit; - - // DMA (circular linked list - never modified during operation) - gdma_channel_handle_t _dmaChannel; - dma_descriptor_t* _dmaDesc[2]; - uint8_t* _dmaBuffer[2]; - volatile uint8_t _activeBuffer; - - // Timing - LedTiming _timing; - size_t _bufferSize; - - // Channels - struct ChannelData { - LcdBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - size_t srcPos; - bool active; - }; - ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; - uint8_t _channelCount; - uint16_t _channelMask; - uint16_t _stagedMask; - size_t _maxDataLen; - - static LcdBusContext* _instance; - static uint8_t _refCount; - - friend class LcdBus; + LcdBusContext(); + ~LcdBusContext(); + + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + void fillBuffer(uint8_t bufIdx); + + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data); + + volatile DriverState _state; + bool _initialized; + bool _use16Bit; + + // DMA (circular linked list - never modified during operation) + gdma_channel_handle_t _dmaChannel; + dma_descriptor_t* _dmaDesc[2]; + uint8_t* _dmaBuffer[2]; + volatile uint8_t _activeBuffer; + + // Timing + LedTiming _timing; + size_t _bufferSize; + + // Channels + struct ChannelData { + LcdBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + size_t srcPos; + bool active; + }; + ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; + uint8_t _channelCount; + uint16_t _channelMask; + uint16_t _stagedMask; + size_t _maxDataLen; + + static LcdBusContext* _instance; + static uint8_t _refCount; + + friend class LcdBus; }; class LcdBus : public IBus { public: - LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, - size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, bool use16Bit = false); - ~LcdBus() override; + LcdBus(int8_t pin, const LedTiming& timing, ColorOrder order, + size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, bool use16Bit = false); + ~LcdBus() override; - bool begin() override; - void end() override; + bool begin() override; + void end() override; - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "LCD"; } + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "LCD"; } - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order); + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); private: - bool allocateBuffer(uint16_t numPixels); + bool allocateBuffer(uint16_t numPixels); - int8_t _pin; - size_t _bufferSize; - bool _use16Bit; - LedTiming _timing; - ColorOrder _order; - bool _initialized; + int8_t _pin; + size_t _bufferSize; + bool _use16Bit; + LedTiming _timing; + ColorOrder _order; + bool _initialized; - int8_t _channelIdx; - LcdBusContext* _ctx; + int8_t _channelIdx; + LcdBusContext* _ctx; - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; - size_t _encodedLen; + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; + size_t _encodedLen; }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index fb951c5a30..98f8e05e1d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -21,13 +21,13 @@ namespace WLEDpixelBus { // low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) static inline void spi_ll_apply_config(spi_dev_t *hw) { - hw->cmd.update = 1; - //while (hw->cmd.update); //waiting config applied + hw->cmd.update = 1; + //while (hw->cmd.update); //waiting config applied } static inline void spi_ll_user_start(spi_dev_t *hw) { - hw->cmd.usr = 1; + hw->cmd.usr = 1; } @@ -44,130 +44,130 @@ SpiBusContext* SpiBusContext::_instance = nullptr; uint8_t SpiBusContext::_refCount = 0; SpiBusContext* SpiBusContext::get() { - if (_instance == nullptr) { - _instance = new SpiBusContext(); - } - _refCount++; - return _instance; + if (_instance == nullptr) { + _instance = new SpiBusContext(); + } + _refCount++; + return _instance; } void SpiBusContext::release() { - if (_refCount == 0) return; - _refCount--; - if (_refCount == 0 && _instance) { - delete _instance; - _instance = nullptr; - } + if (_refCount == 0) return; + _refCount--; + if (_refCount == 0 && _instance) { + delete _instance; + _instance = nullptr; + } } SpiBusContext::SpiBusContext() - : _sending(false) - , _initialized(false) - , _hasStarted(false) - , _currentBuffer(0) - , _isrHandle(nullptr) - , _spiIsrHandle(nullptr) - , _hw(&GPSPI2) - , _channelCount(0) - , _framePos(0) - , _numBytes(0) - , _lastTransmitMs(0) - , _stagedMask(0) - , _channelMask(0) + : _sending(false) + , _initialized(false) + , _hasStarted(false) + , _currentBuffer(0) + , _isrHandle(nullptr) + , _spiIsrHandle(nullptr) + , _hw(&GPSPI2) + , _channelCount(0) + , _framePos(0) + , _numBytes(0) + , _lastTransmitMs(0) + , _stagedMask(0) + , _channelMask(0) { - _dmaBuffer[0] = _dmaBuffer[1] = nullptr; - for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, false}; - } + _dmaBuffer[0] = _dmaBuffer[1] = nullptr; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + _channels[i] = {nullptr, -1, nullptr, 0, false}; + } } SpiBusContext::~SpiBusContext() { - deinit(); + deinit(); } bool SpiBusContext::isSpiDone() { - if (!_hasStarted) return true; // no transfer ever started - //return txdone; - // We are logically done once all real data is encoded TODO: should add a timeout here for more accurate end of frame timing - if (!_sending) { - //delay(10); // wait for finish sending !!! this is a hacky test, to check if this locks stuff up. remove again -> seems to fix the glitching and stalls, need a better solution though - return txdone; - } + if (!_hasStarted) return true; // no transfer ever started + //return txdone; + // We are logically done once all real data is encoded TODO: should add a timeout here for more accurate end of frame timing + if (!_sending) { + //delay(10); // wait for finish sending !!! this is a hacky test, to check if this locks stuff up. remove again -> seems to fix the glitching and stalls, need a better solution though + return txdone; + } - // Fail-safe watchdog: if stuck for > 500ms, recover it - if (millis() - _lastTransmitMs > 50) { - forceIdle(); - return true; - } - - return false; + // Fail-safe watchdog: if stuck for > 500ms, recover it + if (millis() - _lastTransmitMs > 50) { + forceIdle(); + return true; + } + + return false; } void SpiBusContext::forceIdle() { - _sending = false; - isSending = false; // TODO: integrate this into driver, i.e. non global - _stagedMask = 0; + _sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global + _stagedMask = 0; - spi_ll_dma_tx_fifo_reset(_hw); - spi_ll_outfifo_empty_clr(_hw); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); - // Stop DMA immediately - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + // Stop DMA immediately + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - if (_hw) { - _hw->cmd.usr = 0; // stop SPI user transfer (will output a fast clock, so detach pins from spi before) - _hw->dma_int_clr.val = 0xFFFFFFFF; // - } + if (_hw) { + _hw->cmd.usr = 0; // stop SPI user transfer (will output a fast clock, so detach pins from spi before) + _hw->dma_int_clr.val = 0xFFFFFFFF; // + } } void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { - uint8_t* dst = _dmaBuffer[bufIdx]; - memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); + uint8_t* dst = _dmaBuffer[bufIdx]; + memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); - if (!_sending) { - return; - } + if (!_sending) { + return; + } - size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte - size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; - size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; + size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte + size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; + size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; - if (srcThisChunk == 0) { - _sending = false; - isSending = false; // TODO: integrate this into driver, i.e. non global - _framePos = 0; - return; - } + if (srcThisChunk == 0) { + _sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global + _framePos = 0; + return; + } - for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { - if (!_channels[lane].active || !_channels[lane].srcData) continue; - - size_t srcLen = _channels[lane].srcLen; - if (_framePos >= srcLen) continue; // Past the end of this lane's data, leave buffer 0 (low/no pulse) - - size_t validBytes = srcLen - _framePos; - if (validBytes > srcThisChunk) validBytes = srcThisChunk; - - const uint16_t zerobit = SPI_ZERO_BIT << lane; - const uint16_t onebit = SPI_ONE_BIT << lane; - const uint8_t* src = _channels[lane].srcData; - uint16_t* pOut = reinterpret_cast(dst); - - for (size_t i = 0; i < validBytes; i++) { - uint8_t v = src[_framePos + i]; - *pOut++ |= (v & 0x80) ? onebit : zerobit; - *pOut++ |= (v & 0x40) ? onebit : zerobit; - *pOut++ |= (v & 0x20) ? onebit : zerobit; - *pOut++ |= (v & 0x10) ? onebit : zerobit; - *pOut++ |= (v & 0x08) ? onebit : zerobit; - *pOut++ |= (v & 0x04) ? onebit : zerobit; - *pOut++ |= (v & 0x02) ? onebit : zerobit; - *pOut++ |= (v & 0x01) ? onebit : zerobit; - } + for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { + if (!_channels[lane].active || !_channels[lane].srcData) continue; + + size_t srcLen = _channels[lane].srcLen; + if (_framePos >= srcLen) continue; // Past the end of this lane's data, leave buffer 0 (low/no pulse) + + size_t validBytes = srcLen - _framePos; + if (validBytes > srcThisChunk) validBytes = srcThisChunk; + + const uint16_t zerobit = SPI_ZERO_BIT << lane; + const uint16_t onebit = SPI_ONE_BIT << lane; + const uint8_t* src = _channels[lane].srcData; + uint16_t* pOut = reinterpret_cast(dst); + + for (size_t i = 0; i < validBytes; i++) { + uint8_t v = src[_framePos + i]; + *pOut++ |= (v & 0x80) ? onebit : zerobit; + *pOut++ |= (v & 0x40) ? onebit : zerobit; + *pOut++ |= (v & 0x20) ? onebit : zerobit; + *pOut++ |= (v & 0x10) ? onebit : zerobit; + *pOut++ |= (v & 0x08) ? onebit : zerobit; + *pOut++ |= (v & 0x04) ? onebit : zerobit; + *pOut++ |= (v & 0x02) ? onebit : zerobit; + *pOut++ |= (v & 0x01) ? onebit : zerobit; } - _framePos += srcThisChunk; + } + _framePos += srcThisChunk; } // SPI ISR: handles trans_done (restart SPI bit counter) and outfifo_empty_err @@ -179,23 +179,23 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags if (raw & SPI_TRANS_DONE_INT_ST) { - txdone = true; // transfer finished + txdone = true; // transfer finished // Serial.print("b"); } else { // Serial.println(raw, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST - // this may be some SPI error, stop SPI and DMA to prevent lockup - // detach pins from spi and force low to prevent any output when cmd.user is stopped (which outputs a fast pattern) - for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { - gpio_matrix_out(ctx->_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); - gpio_set_level((gpio_num_t)ctx->_channels[i].pin, 0); - } + // this may be some SPI error, stop SPI and DMA to prevent lockup + // detach pins from spi and force low to prevent any output when cmd.user is stopped (which outputs a fast pattern) + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { + gpio_matrix_out(ctx->_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); + gpio_set_level((gpio_num_t)ctx->_channels[i].pin, 0); } - //ctx->_hw->cmd.usr = 0; // Stop SPI -> this cauuses a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) - //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks - ctx->forceIdle(); - txdone = true; // still mark it as done + } + //ctx->_hw->cmd.usr = 0; // Stop SPI -> this cauuses a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) + //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks + ctx->forceIdle(); + txdone = true; // still mark it as done } @@ -204,220 +204,220 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { } void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { - SpiBusContext* ctx = (SpiBusContext*)arg; - gdma_dev_t* dma = &GDMA; + SpiBusContext* ctx = (SpiBusContext*)arg; + gdma_dev_t* dma = &GDMA; // Serial.print("*"); - // toggle gpio0 + // toggle gpio0 // digitalWrite(0, HIGH); - if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { - uint8_t completedBuf = ctx->_currentBuffer; - ctx->encodeSpiChunk(completedBuf); - // if (isSending) { - - // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI - // ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; - // ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; - //ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? - //ctx->_dmaDesc[completedBuf].owner = 1; - // } - ctx->_currentBuffer = completedBuf ? 0 : 1; // toggle DMA buffer - } + if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + uint8_t completedBuf = ctx->_currentBuffer; + ctx->encodeSpiChunk(completedBuf); + // if (isSending) { + + // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI + // ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + // ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + //ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? + //ctx->_dmaDesc[completedBuf].owner = 1; + // } + ctx->_currentBuffer = completedBuf ? 0 : 1; // toggle DMA buffer + } // digitalWrite(0, LOW); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel } bool SpiBusContext::init(const LedTiming& timing) { - if (_initialized) return true; - - //pinMode(0, OUTPUT); - //digitalWrite(0, LOW); //!!! - - // Allocate DMA buffers - for (int i = 0; i < 2; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, - MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); - if (!_dmaBuffer[i]) { - Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, WLEDPB_SPI_DMA_BUFFER_SIZE); + if (_initialized) return true; + + //pinMode(0, OUTPUT); + //digitalWrite(0, LOW); //!!! + + // Allocate DMA buffers + for (int i = 0; i < 2; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, + MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (!_dmaBuffer[i]) { + Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); + deinit(); + return false; } + memset(_dmaBuffer[i], 0, WLEDPB_SPI_DMA_BUFFER_SIZE); + } - // Setup DMA descriptors - circular linked list - for (int i = 0; i < 2; i++) { - _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; - _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; - _dmaDesc[i].owner = 1; - _dmaDesc[i].sosf = 0; - _dmaDesc[i].eof = 1; - _dmaDesc[i].buf = _dmaBuffer[i]; - } - _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; - _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; - - // Enable peripheral clocks and force-reset (SPI2 + DMA) - // periph_module_enable() uses ref counting and may be a no-op if the - // peripheral was already enabled. Explicit reset ensures clean state. - periph_module_enable(PERIPH_SPI2_MODULE); - periph_module_reset(PERIPH_SPI2_MODULE); - periph_module_enable(PERIPH_GDMA_MODULE); - periph_module_reset(PERIPH_GDMA_MODULE); - - // Configure SPI2 master - spi_ll_master_init(_hw); - spi_ll_master_set_mode(_hw, 0); - spi_ll_set_tx_lsbfirst(_hw, true); - - // skip all SPI phases and jump directly to user mosi phase - _hw->user.usr_command = 0; - _hw->user.usr_addr = 0; - _hw->user.usr_dummy = 0; - _hw->user.usr_miso = 0; - _hw->user.usr_mosi = 1; - - // Clear idle output polarities for D2/D3 - _hw->ctrl.q_pol = 0; - _hw->ctrl.d_pol = 0; - _hw->ctrl.hold_pol = 0; - _hw->ctrl.wp_pol = 0; - - spi_line_mode_t linemode = {}; - linemode.data_lines = 4; // quad mode - spi_ll_master_set_line_mode(_hw, linemode); - - // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config - // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) - // Clock: 4 steps per bit → 4 SPI clock cycles per bit period. - // targetFreq = 4 / (bitPeriod_ns * 1e-9) = 4,000,000,000 / bitPeriod_ns - uint32_t bitPeriodNs = timing.bitPeriod(); - uint32_t targetFreq = 4000000000UL / bitPeriodNs; - if (targetFreq < 2000000) targetFreq = 2000000; - if (targetFreq > 5000000) targetFreq = 5000000; - - spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); - - // Route SPI clock to a dummy pin (needed for DMA to work) -> seems to work fine without this (maybe an IDF V5 issue?) - //pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); + // Setup DMA descriptors - circular linked list + for (int i = 0; i < 2; i++) { + _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].owner = 1; + _dmaDesc[i].sosf = 0; + _dmaDesc[i].eof = 1; + _dmaDesc[i].buf = _dmaBuffer[i]; + } + _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; + _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; + + // Enable peripheral clocks and force-reset (SPI2 + DMA) + // periph_module_enable() uses ref counting and may be a no-op if the + // peripheral was already enabled. Explicit reset ensures clean state. + periph_module_enable(PERIPH_SPI2_MODULE); + periph_module_reset(PERIPH_SPI2_MODULE); + periph_module_enable(PERIPH_GDMA_MODULE); + periph_module_reset(PERIPH_GDMA_MODULE); + + // Configure SPI2 master + spi_ll_master_init(_hw); + spi_ll_master_set_mode(_hw, 0); + spi_ll_set_tx_lsbfirst(_hw, true); + + // skip all SPI phases and jump directly to user mosi phase + _hw->user.usr_command = 0; + _hw->user.usr_addr = 0; + _hw->user.usr_dummy = 0; + _hw->user.usr_miso = 0; + _hw->user.usr_mosi = 1; + + // Clear idle output polarities for D2/D3 + _hw->ctrl.q_pol = 0; + _hw->ctrl.d_pol = 0; + _hw->ctrl.hold_pol = 0; + _hw->ctrl.wp_pol = 0; + + spi_line_mode_t linemode = {}; + linemode.data_lines = 4; // quad mode + spi_ll_master_set_line_mode(_hw, linemode); + + // Clock: target ~2.6MHz for ~390ns per step, matching user's tested config + // 4 steps per bit → ~1560ns per bit (within WS2812 tolerance) + // Clock: 4 steps per bit → 4 SPI clock cycles per bit period. + // targetFreq = 4 / (bitPeriod_ns * 1e-9) = 4,000,000,000 / bitPeriod_ns + uint32_t bitPeriodNs = timing.bitPeriod(); + uint32_t targetFreq = 4000000000UL / bitPeriodNs; + if (targetFreq < 2000000) targetFreq = 2000000; + if (targetFreq > 5000000) targetFreq = 5000000; + + spi_ll_master_set_clock(_hw, 80000000, targetFreq, 128); + + // Route SPI clock to a dummy pin (needed for DMA to work) -> seems to work fine without this (maybe an IDF V5 issue?) + //pinMatrixOutAttach(11, FSPICLK_OUT_IDX, false, false); // spi_ll_set_mosi_bitlen(_hw, 16384); // dummy init value, not required (set properly when transfer starts) - spi_ll_enable_mosi(_hw, true); - - spi_ll_dma_tx_enable(_hw, true); - spi_ll_dma_tx_fifo_reset(_hw); - spi_ll_outfifo_empty_clr(_hw); - spi_ll_apply_config(_hw); - - // Configure GDMA - gdma_dev_t* dma = &GDMA; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + spi_ll_enable_mosi(_hw, true); + + spi_ll_dma_tx_enable(_hw, true); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + spi_ll_apply_config(_hw); + + // Configure GDMA + gdma_dev_t* dma = &GDMA; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); // gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, - gdmaISR, this, &_isrHandle); - if (err != ESP_OK) { - Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); - deinit(); - return false; - } + esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, + gdmaISR, this, &_isrHandle); + if (err != ESP_OK) { + Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + deinit(); + return false; + } - // Install SPI ISR for trans_done and outfifo_empty_err recovery - _hw->dma_int_clr.val = 0xFFFFFFFF; - _hw->dma_int_ena.trans_done = 1; - _hw->dma_int_ena.outfifo_empty_err = 1; - err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, - spiISR, this, &_spiIsrHandle); - if (err != ESP_OK) { - Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); - deinit(); - return false; - } + // Install SPI ISR for trans_done and outfifo_empty_err recovery + _hw->dma_int_clr.val = 0xFFFFFFFF; + _hw->dma_int_ena.trans_done = 1; + _hw->dma_int_ena.outfifo_empty_err = 1; + err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, + ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, + spiISR, this, &_spiIsrHandle); + if (err != ESP_OK) { + Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); + deinit(); + return false; + } - _initialized = true; - return true; + _initialized = true; + return true; } void SpiBusContext::deinit() { - _sending = false; - isSending = false; // TODO: integrate this into driver, i.e. non global + _sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global - // Stop SPI and DMA before freeing resources - if (_hw) { - _hw->cmd.usr = 0; // Stop SPI transfer - } + // Stop SPI and DMA before freeing resources + if (_hw) { + _hw->cmd.usr = 0; // Stop SPI transfer + } - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // Disable interrupt + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - if (_hw) { - _hw->dma_int_ena.trans_done = 0; // Disable SPI trans_done interrupt - } + if (_hw) { + _hw->dma_int_ena.trans_done = 0; // Disable SPI trans_done interrupt + } - if (_spiIsrHandle) { - esp_intr_free(_spiIsrHandle); - _spiIsrHandle = nullptr; - } + if (_spiIsrHandle) { + esp_intr_free(_spiIsrHandle); + _spiIsrHandle = nullptr; + } - if (_isrHandle) { - esp_intr_free(_isrHandle); - _isrHandle = nullptr; - } + if (_isrHandle) { + esp_intr_free(_isrHandle); + _isrHandle = nullptr; + } - for (int i = 0; i < 2; i++) { - if (_dmaBuffer[i]) { - heap_caps_free(_dmaBuffer[i]); - _dmaBuffer[i] = nullptr; - } + for (int i = 0; i < 2; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; } + } - periph_module_disable(PERIPH_SPI2_MODULE); - periph_module_disable(PERIPH_GDMA_MODULE); - _initialized = false; + periph_module_disable(PERIPH_SPI2_MODULE); + periph_module_disable(PERIPH_GDMA_MODULE); + _initialized = false; } int8_t SpiBusContext::registerChannel(int8_t pin, ParallelSpiBus* bus) { - int8_t idx = -1; - for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - if (!_channels[i].active) { - idx = i; - break; - } + int8_t idx = -1; + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (!_channels[i].active) { + idx = i; + break; } - if (idx < 0) return -1; + } + if (idx < 0) return -1; - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); + _channels[idx].bus = bus; + _channels[idx].pin = pin; + _channels[idx].active = true; + _channelCount++; + _channelMask |= (1 << idx); - // Route SPI data signal to GPIO - pinMode(pin, OUTPUT); - pinMatrixOutAttach(pin, SPI_SIGNAL_INDICES[idx], false, false); + // Route SPI data signal to GPIO + pinMode(pin, OUTPUT); + pinMatrixOutAttach(pin, SPI_SIGNAL_INDICES[idx], false, false); - return idx; + return idx; } void SpiBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; + if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; + if (!_channels[channelIdx].active) return; - if (_channels[channelIdx].pin >= 0) { - gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); - } + if (_channels[channelIdx].pin >= 0) { + gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); + } - _channels[channelIdx] = {nullptr, -1, nullptr, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); + _channels[channelIdx] = {nullptr, -1, nullptr, 0, false}; + _channelCount--; + _channelMask &= ~(1 << channelIdx); } void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { @@ -442,9 +442,9 @@ bool SpiBusContext::startTransmit() { _lastTransmitMs = millis(); size_t newBytes = 0; for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcLen > newBytes) { - newBytes = _channels[ch].srcLen; - } + if (_channels[ch].active && _channels[ch].srcLen > newBytes) { + newBytes = _channels[ch].srcLen; + } } _framePos = 0; @@ -476,9 +476,9 @@ bool SpiBusContext::startTransmit() { // re-attach pins to SPI signals for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - if (_channels[i].active && _channels[i].pin >= 0) { - pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], false, false); // TODO: should this trick also be used to force outputs low during reset pulse? - } + if (_channels[i].active && _channels[i].pin >= 0) { + pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], false, false); // TODO: should this trick also be used to force outputs low during reset pulse? + } } gdma_dev_t* dma = &GDMA; @@ -502,140 +502,140 @@ bool SpiBusContext::startTransmit() { // SpiBus implementation ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order) - : _pin(pin) - , _timing(timing) - , _order(order) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) + : _pin(pin) + , _timing(timing) + , _order(order) + , _initialized(false) + , _channelIdx(-1) + , _ctx(nullptr) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) { } ParallelSpiBus::~ParallelSpiBus() { - end(); + end(); } bool ParallelSpiBus::begin() { - if (_initialized) return true; + if (_initialized) return true; - _ctx = SpiBusContext::get(); - if (!_ctx) return false; + _ctx = SpiBusContext::get(); + if (!_ctx) return false; - if (!_ctx->init(_timing)) { - SpiBusContext::release(); - _ctx = nullptr; - return false; - } + if (!_ctx->init(_timing)) { + SpiBusContext::release(); + _ctx = nullptr; + return false; + } - _channelIdx = _ctx->registerChannel(_pin, this); - if (_channelIdx < 0) { - Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); - SpiBusContext::release(); - _ctx = nullptr; - return false; - } + _channelIdx = _ctx->registerChannel(_pin, this); + if (_channelIdx < 0) { + Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); + SpiBusContext::release(); + _ctx = nullptr; + return false; + } - _initialized = true; - return true; + _initialized = true; + return true; } void ParallelSpiBus::end() { - if (!_initialized) return; - - if (_ctx) { - uint32_t startWait = millis(); - while (!_ctx->isIdle()) { - if (millis() - startWait > 100) { - break; - } - vTaskDelay(1); - } - _ctx->unregisterChannel(_channelIdx); - SpiBusContext::release(); - _ctx = nullptr; - } + if (!_initialized) return; - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; + if (_ctx) { + uint32_t startWait = millis(); + while (!_ctx->isIdle()) { + if (millis() - startWait > 100) { + break; + } + vTaskDelay(1); } + _ctx->unregisterChannel(_channelIdx); + SpiBusContext::release(); + _ctx = nullptr; + } + + if (_encodeBuffer) { + heap_caps_free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } - _initialized = false; + _initialized = false; } bool ParallelSpiBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) return true; + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) return true; - if (_encodeBuffer) heap_caps_free(_encodeBuffer); + if (_encodeBuffer) heap_caps_free(_encodeBuffer); - _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; - return true; + _encodeBuffer = (uint8_t*)heap_caps_malloc(needed, MALLOC_CAP_INTERNAL); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; } // TODO: this actually only uses internal buffer, could get rid of the parameters that are now unused. bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; - - // Wait for previous transmission - /* - uint32_t startWait = millis(); - while (!_ctx->isIdle()) { - if (millis() - startWait > 500) { - _ctx->forceIdle(); - return false; - } - vTaskDelay(1); - }*/ - - // Wait for SPI to finish any remaining transfer - /* - startWait = millis(); - while (!_ctx->isSpiDone()) { - if (millis() - startWait > 500) { - _ctx->forceIdle(); - return false; - } - vTaskDelay(1); - }*/ - - if (!allocateBuffer(_numPixels)) return false; - - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); - - for (uint16_t i = 0; i < _numPixels; i++) { - encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); - dst += numCh; + if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + + // Wait for previous transmission + /* + uint32_t startWait = millis(); + while (!_ctx->isIdle()) { + if (millis() - startWait > 500) { + _ctx->forceIdle(); + return false; + } + vTaskDelay(1); + }*/ + + // Wait for SPI to finish any remaining transfer + /* + startWait = millis(); + while (!_ctx->isSpiDone()) { + if (millis() - startWait > 500) { + _ctx->forceIdle(); + return false; } + vTaskDelay(1); + }*/ + + if (!allocateBuffer(_numPixels)) return false; + + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < _numPixels; i++) { + encoder.encode(_pixelData[i], _cctData ? &_cctData[i] : nullptr, dst); + dst += numCh; + } - _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); + _ctx->setChannelData(_channelIdx, _encodeBuffer, _numPixels * numCh); - return _ctx->startTransmit(); + return _ctx->startTransmit(); } bool ParallelSpiBus::canShow() const { - if (!_ctx) return true; - return _ctx->isSpiDone(); + if (!_ctx) return true; + return _ctx->isSpiDone(); } void ParallelSpiBus::waitComplete() { // while (_ctx && !_ctx->isIdle()) { - vTaskDelay(1); + vTaskDelay(1); // } } void ParallelSpiBus::setColorOrder(ColorOrder order) { - _order = order; + _order = order; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index 79f1ebe3a9..fe2dbc111c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -21,65 +21,65 @@ class ParallelSpiBus; */ class SpiBusContext { public: - static SpiBusContext* get(); - static void release(); + static SpiBusContext* get(); + static void release(); - bool init(const LedTiming& timing); - void deinit(); + bool init(const LedTiming& timing); + void deinit(); - int8_t registerChannel(int8_t pin, ParallelSpiBus* bus); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } + int8_t registerChannel(int8_t pin, ParallelSpiBus* bus); + void unregisterChannel(int8_t channelIdx); + uint8_t getChannelCount() const { return _channelCount; } - bool startTransmit(); - bool isIdle() const { return !_sending; } - bool isSpiDone(); - void forceIdle(); + bool startTransmit(); + bool isIdle() const { return !_sending; } + bool isSpiDone(); + void forceIdle(); - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); + void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); private: - SpiBusContext(); - ~SpiBusContext(); - - void encodeSpiChunk(uint8_t bufIdx); - - static void IRAM_ATTR gdmaISR(void* arg); - static void IRAM_ATTR spiISR(void* arg); - - volatile bool _sending; - bool _initialized; - bool _hasStarted; - volatile uint8_t _currentBuffer; - - // DMA - uint8_t* _dmaBuffer[2]; - lldesc_t _dmaDesc[2]; - intr_handle_t _isrHandle; - intr_handle_t _spiIsrHandle; - - // SPI device - spi_dev_t* _hw; - - // Source data per channel - struct ChannelData { - ParallelSpiBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - bool active; - }; - ChannelData _channels[WLEDPB_SPI_MAX_CHANNELS]; - uint8_t _channelCount; - size_t _framePos; // current source byte position - size_t _numBytes; // total source bytes to send - mutable uint32_t _lastTransmitMs; - - uint8_t _stagedMask; - uint8_t _channelMask; - - static SpiBusContext* _instance; - static uint8_t _refCount; + SpiBusContext(); + ~SpiBusContext(); + + void encodeSpiChunk(uint8_t bufIdx); + + static void IRAM_ATTR gdmaISR(void* arg); + static void IRAM_ATTR spiISR(void* arg); + + volatile bool _sending; + bool _initialized; + bool _hasStarted; + volatile uint8_t _currentBuffer; + + // DMA + uint8_t* _dmaBuffer[2]; + lldesc_t _dmaDesc[2]; + intr_handle_t _isrHandle; + intr_handle_t _spiIsrHandle; + + // SPI device + spi_dev_t* _hw; + + // Source data per channel + struct ChannelData { + ParallelSpiBus* bus; + int8_t pin; + const uint8_t* srcData; + size_t srcLen; + bool active; + }; + ChannelData _channels[WLEDPB_SPI_MAX_CHANNELS]; + uint8_t _channelCount; + size_t _framePos; // current source byte position + size_t _numBytes; // total source bytes to send + mutable uint32_t _lastTransmitMs; + + uint8_t _stagedMask; + uint8_t _channelMask; + + static SpiBusContext* _instance; + static uint8_t _refCount; }; /** @@ -87,34 +87,34 @@ class SpiBusContext { */ class ParallelSpiBus : public IBus { public: - ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); - ~ParallelSpiBus() override; + ParallelSpiBus(int8_t pin, const LedTiming& timing, ColorOrder order); + ~ParallelSpiBus() override; - bool begin() override; - void end() override; + bool begin() override; + void end() override; - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "SPI"; } + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "SPI"; } - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order); + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order); private: - bool allocateBuffer(uint16_t numPixels); + bool allocateBuffer(uint16_t numPixels); - int8_t _pin; - LedTiming _timing; - ColorOrder _order; - bool _initialized; + int8_t _pin; + LedTiming _timing; + ColorOrder _order; + bool _initialized; - int8_t _channelIdx; - SpiBusContext* _ctx; + int8_t _channelIdx; + SpiBusContext* _ctx; - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 4f374022c5..809036d6ed 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -10,9 +10,9 @@ namespace WLEDpixelBus { // Context for RMT translate callback - must be in DRAM for IRAM ISR access static DRAM_ATTR struct { - uint32_t bit0; - uint32_t bit1; - uint16_t resetDuration; + uint32_t bit0; + uint32_t bit1; + uint16_t resetDuration; } s_rmtCtx; // Static auto-channel counter for RmtBus @@ -23,328 +23,328 @@ uint8_t RmtBus::s_usedBlocks = 0; uint8_t RmtBus::s_activeChannelMask = 0; RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t channel) - : _pin(pin) - , _channel(channel) - , _timing(timing) - , _order(order) - , _inverted(false) - , _initialized(false) - , _rmtChannel(RMT_CHANNEL_0) - , _rmtBit0(0) - , _rmtBit1(0) - , _rmtResetTicks(0) - , _encodeBuffer(nullptr) - , _encodeBufferSize(0) + : _pin(pin) + , _channel(channel) + , _timing(timing) + , _order(order) + , _inverted(false) + , _initialized(false) + , _rmtChannel(RMT_CHANNEL_0) + , _rmtBit0(0) + , _rmtBit1(0) + , _rmtResetTicks(0) + , _encodeBuffer(nullptr) + , _encodeBufferSize(0) { } RmtBus::~RmtBus() { - end(); + end(); } void RmtBus::updateRmtTiming() { - // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick - const uint8_t clockDiv = 2; - 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; - }; - - uint16_t t0h = nsToTicks(_timing.t0h_ns); - uint16_t t0l = nsToTicks(_timing.t0l_ns); - uint16_t t1h = nsToTicks(_timing.t1h_ns); - uint16_t t1l = nsToTicks(_timing.t1l_ns); - - rmt_item32_t bit0, bit1; - - if (_inverted) { - bit0.level0 = 0; bit0.duration0 = t0h; - bit0.level1 = 1; bit0.duration1 = t0l; - bit1.level0 = 0; bit1.duration0 = t1h; - bit1.level1 = 1; bit1.duration1 = t1l; - } else { - bit0.level0 = 1; bit0.duration0 = t0h; - bit0.level1 = 0; bit0.duration1 = t0l; - bit1.level0 = 1; bit1.duration0 = t1h; - bit1.level1 = 0; bit1.duration1 = t1l; - } - - _rmtBit0 = bit0.val; - _rmtBit1 = bit1.val; - _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); + // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick + const uint8_t clockDiv = 2; + 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; + }; + + uint16_t t0h = nsToTicks(_timing.t0h_ns); + uint16_t t0l = nsToTicks(_timing.t0l_ns); + uint16_t t1h = nsToTicks(_timing.t1h_ns); + uint16_t t1l = nsToTicks(_timing.t1l_ns); + + rmt_item32_t bit0, bit1; + + if (_inverted) { + bit0.level0 = 0; bit0.duration0 = t0h; + bit0.level1 = 1; bit0.duration1 = t0l; + bit1.level0 = 0; bit1.duration0 = t1h; + bit1.level1 = 1; bit1.duration1 = t1l; + } else { + bit0.level0 = 1; bit0.duration0 = t0h; + bit0.level1 = 0; bit0.duration1 = t0l; + bit1.level0 = 1; bit1.duration0 = t1h; + bit1.level1 = 0; bit1.duration1 = t1l; + } + + _rmtBit0 = bit0.val; + _rmtBit1 = bit1.val; + _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); } bool RmtBus::begin() { - if (_initialized) return true; - - uint8_t blocksToUse = 1; - uint8_t maxTxChannels = getRmtMaxChannels(); - - // Auto-channel select with optimized memory block allocation (assumes no RMT RX usage) - // note on channel allocation: channels are assigned such as to maximize the number of memory blocks - // available to the channel to minimize the number of interrupts needed for buffer re-fills (less context switching overhead) - // C3 and S3 have less channels than blocks, so the last channel can always use additional blocks - // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks - // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 blocks, and CH3 with 5 blocks - if (_channel < 0) { - if (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) { - return false; - } - - uint8_t totalBlocks; + if (_initialized) return true; + + uint8_t blocksToUse = 1; + uint8_t maxTxChannels = getRmtMaxChannels(); + + // Auto-channel select with optimized memory block allocation (assumes no RMT RX usage) + // note on channel allocation: channels are assigned such as to maximize the number of memory blocks + // available to the channel to minimize the number of interrupts needed for buffer re-fills (less context switching overhead) + // C3 and S3 have less channels than blocks, so the last channel can always use additional blocks + // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks + // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 blocks, and CH3 with 5 blocks + if (_channel < 0) { + if (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) { + return false; + } + + uint8_t totalBlocks; #if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S3) - totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory + totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory #elif defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32C3) // note: C6 RMT hardware is the same as C3 - totalBlocks = 4; // other supported ESP32 variants have 4 blocks + totalBlocks = 4; // other supported ESP32 variants have 4 blocks #else - totalBlocks = 4; // default to 4 if unknown, should be safe + totalBlocks = 4; // default to 4 if unknown, should be safe #endif - int left_channels = s_expectedChannels - s_allocatedCount - 1; - - if (left_channels == 0) { - _channel = s_currentChannelIndex; - blocksToUse = totalBlocks - s_usedBlocks; - } else { - int k = totalBlocks / s_expectedChannels; - int max_k_for_index = maxTxChannels - s_currentChannelIndex - left_channels; - if (k > max_k_for_index) k = max_k_for_index; - if (k < 1) k = 1; + int left_channels = s_expectedChannels - s_allocatedCount - 1; - _channel = s_currentChannelIndex; - blocksToUse = k; - } + if (left_channels == 0) { + _channel = s_currentChannelIndex; + blocksToUse = totalBlocks - s_usedBlocks; + } else { + int k = totalBlocks / s_expectedChannels; + int max_k_for_index = maxTxChannels - s_currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; - s_currentChannelIndex += blocksToUse; - s_usedBlocks += blocksToUse; - s_allocatedCount++; + _channel = s_currentChannelIndex; + blocksToUse = k; } - if (_channel >= (int8_t)maxTxChannels) { - Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); - return false; - } - _rmtChannel = (rmt_channel_t)_channel; + s_currentChannelIndex += blocksToUse; + s_usedBlocks += blocksToUse; + s_allocatedCount++; + } - Serial.printf("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); + if (_channel >= (int8_t)maxTxChannels) { + Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); + return false; + } + _rmtChannel = (rmt_channel_t)_channel; - updateRmtTiming(); + Serial.printf("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); - rmt_config_t config = {}; + updateRmtTiming(); - config.rmt_mode = RMT_MODE_TX; - config.channel = _rmtChannel; - config.gpio_num = (gpio_num_t)_pin; - config.mem_block_num = blocksToUse; - config.clk_div = 2; // 40MHz + rmt_config_t config = {}; - config.tx_config.loop_en = false; - config.tx_config.carrier_en = false; - config.tx_config.idle_output_en = true; - config.tx_config.idle_level = _inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW; + config.rmt_mode = RMT_MODE_TX; + config.channel = _rmtChannel; + config.gpio_num = (gpio_num_t)_pin; + config.mem_block_num = blocksToUse; + config.clk_div = 2; // 40MHz + config.tx_config.loop_en = false; + config.tx_config.carrier_en = false; + config.tx_config.idle_output_en = true; + config.tx_config.idle_level = _inverted ? RMT_IDLE_LEVEL_HIGH : RMT_IDLE_LEVEL_LOW; - esp_err_t err = rmt_config(&config); - if (err != ESP_OK) { - return false; - } - // Prioritize RMT over I2S/SPI DMA interrupts (which use LEVEL1) to prevent starvation. - // Try LEVEL3 first, fallback to LEVEL2, then LEVEL1. - int flags = ESP_INTR_FLAG_IRAM; + esp_err_t err = rmt_config(&config); + if (err != ESP_OK) { + return false; + } + + // Prioritize RMT over I2S/SPI DMA interrupts (which use LEVEL1) to prevent starvation. + // Try LEVEL3 first, fallback to LEVEL2, then LEVEL1. + int flags = ESP_INTR_FLAG_IRAM; #ifdef ESP_INTR_FLAG_LEVEL3 - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL3); - if (err != ESP_OK) + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL3); + if (err != ESP_OK) #endif - { + { #ifdef ESP_INTR_FLAG_LEVEL2 - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL2); - if (err != ESP_OK) + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL2); + if (err != ESP_OK) #endif - { - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL1); - } - } - if (err != ESP_OK) { - return false; + { + err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL1); } + } + if (err != ESP_OK) { + return false; + } - // Register hack for memory blocks normally assigned to RX + // Register hack for memory blocks normally assigned to RX #if defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) - #if defined(WLEDPB_ESP32S3) - for (int i = 4; i < 8; i++) { - rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); - } - #elif defined(WLEDPB_ESP32C3) - for (int i = 2; i < 4; i++) { - rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); - } - #endif + #if defined(WLEDPB_ESP32S3) + for (int i = 4; i < 8; i++) { + rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); + } + #elif defined(WLEDPB_ESP32C3) + for (int i = 2; i < 4; i++) { + rmt_set_memory_owner((rmt_channel_t)i, RMT_MEM_OWNER_TX); + } + #endif #endif - err = rmt_translator_init(_rmtChannel, translateCB); - if (err != ESP_OK) { - rmt_driver_uninstall(_rmtChannel); - return false; - } + err = rmt_translator_init(_rmtChannel, translateCB); + if (err != ESP_OK) { + rmt_driver_uninstall(_rmtChannel); + return false; + } - _initialized = true; - s_activeChannelMask |= (1 << _channel); - return true; + _initialized = true; + s_activeChannelMask |= (1 << _channel); + return true; } void RmtBus::end() { - if (!_initialized) return; + if (!_initialized) return; - s_activeChannelMask &= ~(1 << _channel); - rmt_driver_uninstall(_rmtChannel); + s_activeChannelMask &= ~(1 << _channel); + rmt_driver_uninstall(_rmtChannel); - if (_encodeBuffer) { - free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } + if (_encodeBuffer) { + free(_encodeBuffer); + _encodeBuffer = nullptr; + _encodeBufferSize = 0; + } - _initialized = false; + _initialized = false; } bool RmtBus::allocateBuffer(uint16_t numPixels) { - size_t needed = numPixels * getChannelCount(_order); - if (_encodeBuffer && _encodeBufferSize >= needed) { - return true; - } - - if (_encodeBuffer) { - free(_encodeBuffer); - } - - _encodeBuffer = (uint8_t*)malloc(needed); - if (!_encodeBuffer) { - _encodeBufferSize = 0; - return false; - } - _encodeBufferSize = needed; + size_t needed = numPixels * getChannelCount(_order); + if (_encodeBuffer && _encodeBufferSize >= needed) { return true; + } + + if (_encodeBuffer) { + free(_encodeBuffer); + } + + _encodeBuffer = (uint8_t*)malloc(needed); + if (!_encodeBuffer) { + _encodeBufferSize = 0; + return false; + } + _encodeBufferSize = needed; + return true; } bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!pixels) { - pixels = _pixelData; - numPixels = _numPixels; - cct = _cctData; - } - if (numPixels == 0) numPixels = _numPixels; - if (!cct) cct = _cctData; - - if (!_initialized || !pixels || numPixels == 0) { - return false; - } - - // Wait for previous transmission on THIS channel to complete - rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); - - if (!allocateBuffer(numPixels)) return false; - - // Encode pixels to byte stream - ColorEncoder encoder(_order); - uint8_t* dst = _encodeBuffer; - uint8_t numCh = encoder.getNumChannels(); - - for (uint16_t i = 0; i < numPixels; i++) { - encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); - dst += numCh; - } - - // Update context for ISR - s_rmtCtx.bit0 = _rmtBit0; - s_rmtCtx.bit1 = _rmtBit1; - s_rmtCtx.resetDuration = _rmtResetTicks; - - // Start transmission - size_t dataLen = numPixels * numCh; - esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); - - return err == ESP_OK; + if (!pixels) { + pixels = _pixelData; + numPixels = _numPixels; + cct = _cctData; + } + if (numPixels == 0) numPixels = _numPixels; + if (!cct) cct = _cctData; + + if (!_initialized || !pixels || numPixels == 0) { + return false; + } + + // Wait for previous transmission on THIS channel to complete + rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); + + if (!allocateBuffer(numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + dst += numCh; + } + + // Update context for ISR + s_rmtCtx.bit0 = _rmtBit0; + s_rmtCtx.bit1 = _rmtBit1; + s_rmtCtx.resetDuration = _rmtResetTicks; + + // Start transmission + size_t dataLen = numPixels * numCh; + esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); + + return err == ESP_OK; } bool RmtBus::canShow() const { - if (!_initialized) return true; - // Use rmt_wait_tx_done with 0 timeout to check if TX is done (matching NeoPixelBus) - return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); + if (!_initialized) return true; + // Use rmt_wait_tx_done with 0 timeout to check if TX is done (matching NeoPixelBus) + return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); } void RmtBus::waitComplete() { - if (_initialized) { - rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); - } + if (_initialized) { + rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + } } void RmtBus::setTiming(const LedTiming& timing) { - _timing = timing; - if (_initialized) { - updateRmtTiming(); - } + _timing = timing; + if (_initialized) { + updateRmtTiming(); + } } void RmtBus::setColorOrder(ColorOrder order) { - _order = order; + _order = order; } void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num) { - if (src == nullptr || dest == nullptr) { - *translated_size = 0; - *item_num = 0; - return; + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num) { + if (src == nullptr || dest == nullptr) { + *translated_size = 0; + *item_num = 0; + return; + } + + const uint8_t* psrc = (const uint8_t*)src; + rmt_item32_t* pdest = dest; + size_t size = 0; + size_t num = 0; + + uint32_t bit0 = s_rmtCtx.bit0; + uint32_t bit1 = s_rmtCtx.bit1; + uint16_t resetDuration = s_rmtCtx.resetDuration; + + // Each byte produces 8 RMT items + for (;;) + { + uint8_t data = *psrc; + + for (uint8_t bit = 0; bit < 8; bit++) + { + pdest->val = (data & 0x80) ? bit1 : bit0; + pdest++; + data <<= 1; } + num += 8; + size++; - const uint8_t* psrc = (const uint8_t*)src; - rmt_item32_t* pdest = dest; - size_t size = 0; - size_t num = 0; - - uint32_t bit0 = s_rmtCtx.bit0; - uint32_t bit1 = s_rmtCtx.bit1; - uint16_t resetDuration = s_rmtCtx.resetDuration; + // If this is the last byte, extend the last bit's LOW duration + // to include the full reset signal length + if (size >= src_size) + { + pdest--; + pdest->duration1 = resetDuration; + break; + } - // Each byte produces 8 RMT items - for (;;) + if (num >= wanted_num) { - uint8_t data = *psrc; - - for (uint8_t bit = 0; bit < 8; bit++) - { - pdest->val = (data & 0x80) ? bit1 : bit0; - pdest++; - data <<= 1; - } - num += 8; - size++; - - // If this is the last byte, extend the last bit's LOW duration - // to include the full reset signal length - if (size >= src_size) - { - pdest--; - pdest->duration1 = resetDuration; - break; - } - - if (num >= wanted_num) - { - break; - } - - psrc++; + break; } - *translated_size = size; - *item_num = num; + psrc++; + } + + *translated_size = size; + *item_num = num; } } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 47216f5d5f..05903c3f05 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -12,69 +12,69 @@ namespace WLEDpixelBus { class RmtBus : public IBus { public: - /** - * Create RMT bus - * @param pin GPIO pin - * @param timing LED timing - * @param order Color order - * @param channel RMT channel (-1 for auto) - */ - RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, - int8_t channel = -1); - ~RmtBus() override; + /** + * Create RMT bus + * @param pin GPIO pin + * @param timing LED timing + * @param order Color order + * @param channel RMT channel (-1 for auto) + */ + RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, + int8_t channel = -1); + ~RmtBus() override; - bool begin() override; - void end() override; + bool begin() override; + void end() override; - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return "RMT"; } + bool show(const uint32_t* pixels, uint16_t numPixels, + const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return "RMT"; } - // Configuration - void setInverted(bool inv) { _inverted = inv; } - void setTiming(const LedTiming& timing); - void setColorOrder(ColorOrder order); + // Configuration + void setInverted(bool inv) { _inverted = inv; } + void setTiming(const LedTiming& timing); + void setColorOrder(ColorOrder order); - // Reset the auto-allocation counter (call before re-creating buses) - static void setExpectedChannels(uint8_t expected) { s_expectedChannels = (expected > 0) ? expected : 1; } - static void resetAutoChannel() { - s_allocatedCount = 0; - s_currentChannelIndex = 0; - s_usedBlocks = 0; - } + // Reset the auto-allocation counter (call before re-creating buses) + static void setExpectedChannels(uint8_t expected) { s_expectedChannels = (expected > 0) ? expected : 1; } + static void resetAutoChannel() { + s_allocatedCount = 0; + s_currentChannelIndex = 0; + s_usedBlocks = 0; + } private: - int8_t _pin; - int8_t _channel; - LedTiming _timing; - ColorOrder _order; - bool _inverted; - bool _initialized; - - rmt_channel_t _rmtChannel; - uint32_t _rmtBit0; - uint32_t _rmtBit1; - uint16_t _rmtResetTicks; + int8_t _pin; + int8_t _channel; + LedTiming _timing; + ColorOrder _order; + bool _inverted; + bool _initialized; + + rmt_channel_t _rmtChannel; + uint32_t _rmtBit0; + uint32_t _rmtBit1; + uint16_t _rmtResetTicks; - // Encode buffer - uint8_t* _encodeBuffer; - size_t _encodeBufferSize; + // Encode buffer + uint8_t* _encodeBuffer; + size_t _encodeBufferSize; - static uint8_t s_expectedChannels; - static uint8_t s_allocatedCount; - static uint8_t s_currentChannelIndex; - static uint8_t s_usedBlocks; - static uint8_t s_activeChannelMask; // bitmask of initialized channels + static uint8_t s_expectedChannels; + static uint8_t s_allocatedCount; + static uint8_t s_currentChannelIndex; + static uint8_t s_usedBlocks; + static uint8_t s_activeChannelMask; // bitmask of initialized channels - void updateRmtTiming(); - bool allocateBuffer(uint16_t numPixels); + void updateRmtTiming(); + bool allocateBuffer(uint16_t numPixels); - // Static translate callback - static void IRAM_ATTR translateCB(const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num); + // Static translate callback + static void IRAM_ATTR translateCB(const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num); }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp index 18138fe0dd..6b3c313b3c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -3,113 +3,113 @@ namespace WLEDpixelBus { SpiBus::SpiBus(int8_t dataPin, int8_t clockPin, const LedTiming& timing, ColorOrder order, bool useHardwareSpi) - : _dataPin(dataPin), _clockPin(clockPin), _timing(timing), _order(order), - _useHardware(useHardwareSpi), _initialized(false) { + : _dataPin(dataPin), _clockPin(clockPin), _timing(timing), _order(order), + _useHardware(useHardwareSpi), _initialized(false) { } SpiBus::~SpiBus() { - end(); + end(); } bool SpiBus::begin() { - if (_initialized) return true; - - if (_useHardware) { - SPI.begin(); - // Assuming around 2MHz for now - in reality this would use timing parameters - SPI.setFrequency(2000000); - SPI.setDataMode(SPI_MODE0); - } else { - pinMode(_dataPin, OUTPUT); - pinMode(_clockPin, OUTPUT); - digitalWrite(_clockPin, LOW); - digitalWrite(_dataPin, LOW); - } - - _initialized = true; - return true; + if (_initialized) return true; + + if (_useHardware) { + SPI.begin(); + // Assuming around 2MHz for now - in reality this would use timing parameters + SPI.setFrequency(2000000); + SPI.setDataMode(SPI_MODE0); + } else { + pinMode(_dataPin, OUTPUT); + pinMode(_clockPin, OUTPUT); + digitalWrite(_clockPin, LOW); + digitalWrite(_dataPin, LOW); + } + + _initialized = true; + return true; } void SpiBus::end() { - if (!_initialized) return; - if (_useHardware) { - SPI.end(); - } else { - pinMode(_dataPin, INPUT); - pinMode(_clockPin, INPUT); - } - _initialized = false; + if (!_initialized) return; + if (_useHardware) { + SPI.end(); + } else { + pinMode(_dataPin, INPUT); + pinMode(_clockPin, INPUT); + } + _initialized = false; } void SpiBus::sendByte(uint8_t d) { - if (_useHardware) { - SPI.transfer(d); - } else { - // Bitbang SPI - for (uint8_t i = 0; i < 8; i++) { - if (d & 0x80) { - digitalWrite(_dataPin, HIGH); - } else { - digitalWrite(_dataPin, LOW); - } - digitalWrite(_clockPin, HIGH); - d <<= 1; - digitalWrite(_clockPin, LOW); - } + if (_useHardware) { + SPI.transfer(d); + } else { + // Bitbang SPI + for (uint8_t i = 0; i < 8; i++) { + if (d & 0x80) { + digitalWrite(_dataPin, HIGH); + } else { + digitalWrite(_dataPin, LOW); + } + digitalWrite(_clockPin, HIGH); + d <<= 1; + digitalWrite(_clockPin, LOW); } + } } void SpiBus::sendStartFrame() { - for (int i = 0; i < 4; i++) { - sendByte(0x00); - } + for (int i = 0; i < 4; i++) { + sendByte(0x00); + } } void SpiBus::sendEndFrame() { - // Basic end frame for APA102 - for (int i = 0; i < 4; i++) { - sendByte(0xFF); - } + // Basic end frame for APA102 + for (int i = 0; i < 4; i++) { + sendByte(0xFF); + } } bool SpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!pixels) pixels = _pixelData; - if (numPixels == 0) numPixels = _numPixels; - if (!_initialized || !pixels || _numPixels == 0) return false; - - // Output generic APA102/DotStar format - sendStartFrame(); - - for (uint16_t i = 0; i < _numPixels; i++) { - uint32_t c = pixels[i]; - - // For APA102: Global brightness + RGB (0xFF is max brightness) - uint8_t r = getR(c); - uint8_t g = getG(c); - uint8_t b = getB(c); - - // Start byte contains 3 bits of 1 and 5 bits of global brightness (could be dynamic) - sendByte(0xFF); - - // Output according to ColorOrder - switch (_order) { - case ColorOrder::RGB: sendByte(r); sendByte(g); sendByte(b); break; - case ColorOrder::GRB: sendByte(g); sendByte(r); sendByte(b); break; - case ColorOrder::BRG: sendByte(b); sendByte(r); sendByte(g); break; - case ColorOrder::RBG: sendByte(r); sendByte(b); sendByte(g); break; - case ColorOrder::GBR: sendByte(g); sendByte(b); sendByte(r); break; - case ColorOrder::BGR: sendByte(b); sendByte(g); sendByte(r); break; - default: sendByte(b); sendByte(g); sendByte(r); break; // Default APA102 BGR - } + if (!pixels) pixels = _pixelData; + if (numPixels == 0) numPixels = _numPixels; + if (!_initialized || !pixels || _numPixels == 0) return false; + + // Output generic APA102/DotStar format + sendStartFrame(); + + for (uint16_t i = 0; i < _numPixels; i++) { + uint32_t c = pixels[i]; + + // For APA102: Global brightness + RGB (0xFF is max brightness) + uint8_t r = getR(c); + uint8_t g = getG(c); + uint8_t b = getB(c); + + // Start byte contains 3 bits of 1 and 5 bits of global brightness (could be dynamic) + sendByte(0xFF); + + // Output according to ColorOrder + switch (_order) { + case ColorOrder::RGB: sendByte(r); sendByte(g); sendByte(b); break; + case ColorOrder::GRB: sendByte(g); sendByte(r); sendByte(b); break; + case ColorOrder::BRG: sendByte(b); sendByte(r); sendByte(g); break; + case ColorOrder::RBG: sendByte(r); sendByte(b); sendByte(g); break; + case ColorOrder::GBR: sendByte(g); sendByte(b); sendByte(r); break; + case ColorOrder::BGR: sendByte(b); sendByte(g); sendByte(r); break; + default: sendByte(b); sendByte(g); sendByte(r); break; // Default APA102 BGR } + } - sendEndFrame(); + sendEndFrame(); - return true; + return true; } bool SpiBus::canShow() const { - return true; + return true; } void SpiBus::waitComplete() { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h index c3b172ad5a..98c3cb326a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -11,39 +11,39 @@ namespace WLEDpixelBus { class SpiBus : public IBus { public: - /** - * Create SPI bus - * @param dataPin MOSI pin - * @param clockPin SCLK pin - * @param timing LED timing parameters (used mostly for bit rate/clock speed) - * @param order Color order - * @param useHardwareSpi True to use hardware SPI peripheral (faster), false for bit-bang - */ - SpiBus(int8_t dataPin, int8_t clockPin, const LedTiming& timing, ColorOrder order, bool useHardwareSpi = true); - ~SpiBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct = nullptr) override; - bool canShow() const override; - void waitComplete() override; - const char* getType() const override { return _useHardware ? "HW_SPI" : "SW_SPI"; } - - void setTiming(const LedTiming& timing) { _timing = timing; } - void setColorOrder(ColorOrder order) { _order = order; } + /** + * Create SPI bus + * @param dataPin MOSI pin + * @param clockPin SCLK pin + * @param timing LED timing parameters (used mostly for bit rate/clock speed) + * @param order Color order + * @param useHardwareSpi True to use hardware SPI peripheral (faster), false for bit-bang + */ + SpiBus(int8_t dataPin, int8_t clockPin, const LedTiming& timing, ColorOrder order, bool useHardwareSpi = true); + ~SpiBus() override; + + bool begin() override; + void end() override; + + bool show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct = nullptr) override; + bool canShow() const override; + void waitComplete() override; + const char* getType() const override { return _useHardware ? "HW_SPI" : "SW_SPI"; } + + void setTiming(const LedTiming& timing) { _timing = timing; } + void setColorOrder(ColorOrder order) { _order = order; } private: - int8_t _dataPin; - int8_t _clockPin; - LedTiming _timing; - ColorOrder _order; - bool _useHardware; - bool _initialized; - - void sendByte(uint8_t d); - void sendStartFrame(); - void sendEndFrame(); + int8_t _dataPin; + int8_t _clockPin; + LedTiming _timing; + ColorOrder _order; + bool _useHardware; + bool _initialized; + + void sendByte(uint8_t d); + void sendStartFrame(); + void sendEndFrame(); }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 037d2006c3..8dc9611e6c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -13,59 +13,59 @@ namespace WLEDpixelBus { * LED timing parameters in nanoseconds */ struct LedTiming { - uint16_t t0h_ns; // '0' bit high time - uint16_t t0l_ns; // '0' bit low time - uint16_t t1h_ns; // '1' bit high time - uint16_t t1l_ns; // '1' bit low time - uint32_t reset_us; // Reset/latch time in microseconds + 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) {} + 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 TODO: drop 3 step cadence support? constexpr uint32_t bitPeriod(uint8_t cadenceSteps = 0) const { - return (cadenceSteps == 4) ? (t0h_ns * cadenceSteps) : ((t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2); + return (cadenceSteps == 4) ? (t0h_ns * cadenceSteps) : ((t0h_ns + t0l_ns + t1h_ns + t1l_ns) / 2); } }; // Predefined timing constants namespace Timing { - // ---- Standard 1-wire LEDs (800KHz family) ---- - constexpr LedTiming WS2812 {300, 900, 700, 500, 100}; // WS2812B - constexpr LedTiming WS2811 {300, 900, 700, 500, 300}; // WS2811 (12V) - constexpr LedTiming WS2813 {300, 850, 700, 350, 300}; // WS2813 (backup data) - constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; // WS2815 (12V, 255mA) - constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; // WS2805 RGBCW (5ch) - constexpr LedTiming SK6812 {300, 900, 800, 450, 200}; // SK6812 / SK6812 RGBW + // ---- Standard 1-wire LEDs (800KHz family) ---- + constexpr LedTiming WS2812 {300, 900, 700, 500, 100}; // WS2812B + constexpr LedTiming WS2811 {300, 900, 700, 500, 300}; // WS2811 (12V) + constexpr LedTiming WS2813 {300, 850, 700, 350, 300}; // WS2813 (backup data) + constexpr LedTiming WS2815 {400, 850, 800, 450, 300}; // WS2815 (12V, 255mA) + constexpr LedTiming WS2805 {300, 790, 790, 300, 300}; // WS2805 RGBCW (5ch) + constexpr LedTiming SK6812 {300, 900, 800, 450, 200}; // SK6812 / SK6812 RGBW - // ---- High-speed WS281x (1MHz) ---- - constexpr LedTiming WS281x_FAST {330, 670, 660, 340, 100}; // WS281x_FAST: 1MHz, 330ns low, 660ns high, 1000ns period - // ---- Titan Micro LEDs ---- - constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; // TM1814 RGBW, requires prefix - constexpr LedTiming TM1815 {740, 1780, 1440, 1060, 200}; // TM1815 RGBW, requires prefix - constexpr LedTiming TM1829 {300, 900, 800, 400, 200}; // TM1829 (inverted logic) - constexpr LedTiming TM1914 {360, 890, 720, 530, 200}; // TM1914 RGB, requires prefix + // ---- High-speed WS281x (1MHz) ---- + constexpr LedTiming WS281x_FAST {330, 670, 660, 340, 100}; // WS281x_FAST: 1MHz, 330ns low, 660ns high, 1000ns period + // ---- Titan Micro LEDs ---- + constexpr LedTiming TM1814 {360, 890, 720, 530, 200}; // TM1814 RGBW, requires prefix + constexpr LedTiming TM1815 {740, 1780, 1440, 1060, 200}; // TM1815 RGBW, requires prefix + constexpr LedTiming TM1829 {300, 900, 800, 400, 200}; // TM1829 (inverted logic) + constexpr LedTiming TM1914 {360, 890, 720, 530, 200}; // TM1914 RGB, requires prefix - // ---- Other 1-wire LEDs ---- - constexpr LedTiming APA106 {350, 1350, 1350, 350, 50}; // APA106 / PL9823 - constexpr LedTiming UCS8903 {400, 850, 800, 450, 500}; // UCS8903 RGB (16bit) - constexpr LedTiming UCS8904 {400, 850, 800, 450, 500}; // UCS8904 RGBW (16bit) - constexpr LedTiming SM16703 {300, 900, 900, 300, 80}; // SM16703 - constexpr LedTiming SM16825 {300, 900, 900, 300, 80}; // SM16825 RGBCW (16bit, 5ch) - constexpr LedTiming FW1906 {400, 850, 800, 450, 300}; // FW1906 GRBCW (5ch) + // ---- Other 1-wire LEDs ---- + constexpr LedTiming APA106 {350, 1350, 1350, 350, 50}; // APA106 / PL9823 + constexpr LedTiming UCS8903 {400, 850, 800, 450, 500}; // UCS8903 RGB (16bit) + constexpr LedTiming UCS8904 {400, 850, 800, 450, 500}; // UCS8904 RGBW (16bit) + constexpr LedTiming SM16703 {300, 900, 900, 300, 80}; // SM16703 + constexpr LedTiming SM16825 {300, 900, 900, 300, 80}; // SM16825 RGBCW (16bit, 5ch) + constexpr LedTiming FW1906 {400, 850, 800, 450, 300}; // FW1906 GRBCW (5ch) - // ---- 400 KHz LEDs ---- - constexpr LedTiming Generic800Kbps {400, 850, 800, 450, 300}; - constexpr LedTiming Generic400Kbps {800, 1700, 1600, 900, 300}; + // ---- 400 KHz LEDs ---- + constexpr LedTiming Generic800Kbps {400, 850, 800, 450, 300}; + constexpr LedTiming Generic400Kbps {800, 1700, 1600, 900, 300}; - // ---- 2-wire (SPI) LEDs - timing represents SPI clock speed ---- - // For SPI LEDs the timing struct is repurposed: bitPeriod() gives the SPI clock period - // Default SPI clock ~2MHz (500ns period) - constexpr LedTiming APA102 {250, 250, 250, 250, 0}; // APA102 / DotStar (up to ~20MHz) - constexpr LedTiming WS2801 {500, 500, 500, 500, 1000}; // WS2801 (max ~2MHz, 500us latch) - constexpr LedTiming LPD8806 {250, 250, 250, 250, 0}; // LPD8806 (latch = 0-bits) - constexpr LedTiming LPD6803 {250, 250, 250, 250, 0}; // LPD6803 - constexpr LedTiming P9813 {250, 250, 250, 250, 0}; // P9813 (Total Control Lighting) + // ---- 2-wire (SPI) LEDs - timing represents SPI clock speed ---- + // For SPI LEDs the timing struct is repurposed: bitPeriod() gives the SPI clock period + // Default SPI clock ~2MHz (500ns period) + constexpr LedTiming APA102 {250, 250, 250, 250, 0}; // APA102 / DotStar (up to ~20MHz) + constexpr LedTiming WS2801 {500, 500, 500, 500, 1000}; // WS2801 (max ~2MHz, 500us latch) + constexpr LedTiming LPD8806 {250, 250, 250, 250, 0}; // LPD8806 (latch = 0-bits) + constexpr LedTiming LPD6803 {250, 250, 250, 250, 0}; // LPD6803 + constexpr LedTiming P9813 {250, 250, 250, 250, 0}; // P9813 (Total Control Lighting) } // ---- I2S / LCD Clock Calculation Helpers ---- @@ -78,10 +78,10 @@ namespace Timing { * @return Clock divider value */ inline uint32_t calcClockDiv(const LedTiming& timing, uint8_t cadenceSteps, uint32_t baseClockMHz) { - uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); - uint32_t stepPeriodNs = bitPeriodNs / cadenceSteps; - uint32_t div = (baseClockMHz * stepPeriodNs) / 1000; - return (div < 2) ? 2 : (div > 255) ? 255 : div; + uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); + uint32_t stepPeriodNs = bitPeriodNs / cadenceSteps; + uint32_t div = (baseClockMHz * stepPeriodNs) / 1000; + return (div < 2) ? 2 : (div > 255) ? 255 : div; } /** @@ -91,10 +91,10 @@ inline uint32_t calcClockDiv(const LedTiming& timing, uint8_t cadenceSteps, uint * @return Number of zero-bytes needed for reset period */ inline uint32_t calcResetBytes(const LedTiming& timing, uint8_t cadenceSteps) { - uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); - uint32_t clockPeriodNs = bitPeriodNs / cadenceSteps; - uint32_t bytesPerUs = (clockPeriodNs > 0) ? (1000 / clockPeriodNs) : 1; - return timing.reset_us * bytesPerUs; + uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); + uint32_t clockPeriodNs = bitPeriodNs / cadenceSteps; + uint32_t bytesPerUs = (clockPeriodNs > 0) ? (1000 / clockPeriodNs) : 1; + return timing.reset_us * bytesPerUs; } } // namespace WLEDpixelBus From 995873484818c195a7e4482a7ea33c2ebcddc164 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 15 Mar 2026 16:54:34 +0100 Subject: [PATCH 044/173] add preliminary 3 step cadence encoding --- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index a694132424..1feee5a94c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -359,6 +359,66 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { // Rest of buffer remains zero (reset signal) from memset } + +void IRAM_ATTR LcdBusContext::encode3Step(uint8_t* dest, size_t destLen) { + // 3-step cadence encoding for parallel output + // Each source bit becomes 3 DMA words (one bit per channel in each 16-bit word) + // Desired output: [HIGH][data][LOW] for each bit + // Buffer is always filled completely (zeros = LOW = reset signal) + + memset(dest, 0, destLen); + size_t pos = 0; + + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) maxCh = ch + 1; + } + + while (pos + 48 <= destLen) { // 8 bits * 3 steps * 2 bytes = 48 bytes per source byte + bool hasData = false; + + for (int ch = 0; ch < maxCh; ch++) { + if (!_channels[ch].active) continue; + if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; + + hasData = true; + uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; + uint16_t chMask = (1 << ch); + + uint16_t* p = (uint16_t*)(dest + pos); + + // Unrolled loop for 8 bits + // [HIGH][data][LOW] — step 0 always high, step 1 = data bit, step 2 always low (zero from memset) + // bit 7 + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; } p += 3; + // bit 6 + p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; } p += 3; + // bit 5 + p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; } p += 3; + // bit 4 + p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; } p += 3; + // bit 3 + p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; } p += 3; + // bit 2 + p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; } p += 3; + // bit 1 + p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; } p += 3; + // bit 0 + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; } p += 3; + } + + if (!hasData) break; + + for (int ch = 0; ch < maxCh; ch++) { + if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { + _channels[ch].srcPos++; + } + } + + pos += 48; + } +} + void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { encode4Step(_dmaBuffer[bufIdx], _bufferSize); } From 2ed3b7fa38ee4788023b5adee7b66e22a94fa261 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 15 Mar 2026 16:59:04 +0100 Subject: [PATCH 045/173] rename define --- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 3b9cb50fa0..7361070199 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -52,10 +52,10 @@ by @dedehai, 2026 // SPI parallel support (C3 - uses SPI quad mode with GDMA) #if defined(WLEDPB_ESP32C3) - #define WLEDPB_SPI_SUPPORT + #define WLEDPB_PARALLEL_SPI_SUPPORT #endif -#ifdef WLEDPB_SPI_SUPPORT +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT #include "soc/spi_struct.h" #include "soc/gdma_struct.h" #include "hal/gdma_ll.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 98f8e05e1d..48bcc6e6ca 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -1,5 +1,5 @@ #include "WLEDpixelBus.h" -#ifdef WLEDPB_SPI_SUPPORT +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT #include "WLEDpixelBus_ParallelSpi.h" #undef FLAG_ATTR @@ -640,4 +640,4 @@ void ParallelSpiBus::setColorOrder(ColorOrder order) { } // namespace WLEDpixelBus -#endif // WLEDPB_SPI_SUPPORT \ No newline at end of file +#endif // WLEDPB_PARALLEL_SPI_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index fe2dbc111c..5e1fc5a304 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -1,7 +1,7 @@ #pragma once #include "WLEDpixelBus.h" -#ifdef WLEDPB_SPI_SUPPORT +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT namespace WLEDpixelBus { //============================================================================== @@ -119,4 +119,4 @@ class ParallelSpiBus : public IBus { } // namespace WLEDpixelBus -#endif // WLEDPB_SPI_SUPPORT +#endif // WLEDPB_PARALLEL_SPI_SUPPORT From 6a1a8ff20e4a7361feb878bb3bfb80ecce43fdaf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 15 Mar 2026 18:28:31 +0100 Subject: [PATCH 046/173] adding dynamic timing, WIP --- wled00/bus_manager.cpp | 3 ++- wled00/bus_manager.h | 7 ++++++- wled00/bus_wrapper.h | 12 ++++++++--- wled00/cfg.cpp | 4 +++- wled00/data/settings_leds.htm | 17 ++++++++++++++++ wled00/set.cpp | 4 +++- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 20 +++++++++---------- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 16 +++++++++++++++ 9 files changed, 68 insertions(+), 19 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index da0c387bfc..67543d12c2 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -135,6 +135,7 @@ BusDigital::BusDigital(const BusConfig &bc) , _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); + _busSpeedFactor = bc.busSpeedFactor; if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; @@ -157,7 +158,7 @@ BusDigital::BusDigital(const BusConfig &bc) if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus // create bus via PolyBus wrapper which will return a WLEDpixelBus::IBus - _busPtr = PolyBus::create(bc.type, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType); + _busPtr = PolyBus::create(bc.type, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType, bc.busSpeedFactor); _valid = (_busPtr != nullptr) && bc.count > 0; // fix for wled#4759 diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index da96dacd99..058e20648a 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -167,6 +167,7 @@ class Bus { 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; } 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 @@ -240,6 +241,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; }; @@ -466,8 +469,9 @@ struct BusConfig { uint16_t milliAmpsMax; uint8_t driverType; // 0=RMT (default), 1=I2S String text; + uint8_t busSpeedFactor; // percent (100 = default) - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "") + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "", uint8_t bsf = 100) : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) @@ -479,6 +483,7 @@ struct BusConfig { , milliAmpsMax(maMax) , driverType(driver) , text(sometext) + , busSpeedFactor(bsf) { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index c7bdb707d8..2854f3f445 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -141,7 +141,7 @@ class PolyBus { return true; } -static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder, uint8_t driverType = 0) { +static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder, uint8_t driverType = 0, uint8_t busSpeedFactor = 100) { if (!Bus::isDigital(busType)) return nullptr; #ifndef ESP8266 @@ -151,6 +151,12 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #endif ProtocolDef proto = getProtocol(busType); + // Apply optional bus speed factor (percent) + WLEDpixelBus::LedTiming timing = proto.timing; + if (busSpeedFactor != 100) { + float factor = (float)busSpeedFactor / 100.0f; + timing = WLEDpixelBus::scaleTiming(proto.timing, factor); + } // Map WLED Order uint8_t wledOrder = (uint8_t)colorOrder & 0x0F; @@ -195,7 +201,7 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #else if (_2PchannelsAssigned == 1) isHSPI = true; #endif - return new WLEDpixelBus::SpiBus(pins[0], pins[1], proto.timing, finalOrder, isHSPI); + return new WLEDpixelBus::SpiBus(pins[0], pins[1], timing, finalOrder, isHSPI); } auto btype = WLEDpixelBus::BusType::Auto; @@ -236,7 +242,7 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #endif - return WLEDpixelBus::createBus(btype, pins[0], proto.timing, finalOrder, WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, rmtCh); + return WLEDpixelBus::createBus(btype, pins[0], timing, finalOrder, WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, rmtCh); } static unsigned memUsage(uint8_t busType, unsigned count, const uint8_t* pins, unsigned driverType = 0) { diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 4f131cab96..67df0066b3 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -246,7 +246,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available String host = elm[F("text")] | String(); - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); + uint8_t bsf = (uint8_t)(elm[F("bsf")] | 100); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host, (uint8_t)bsf); doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } @@ -1000,6 +1001,7 @@ void serializeConfig(JsonObject root) { ins[F("ledma")] = bus->getLEDCurrent(); ins[F("drv")] = bus->getDriverType(); ins[F("text")] = bus->getCustomText(); + ins[F("bsf")] = bus->getBusSpeedFactor(); } JsonArray hw_com = hw.createNestedArray(F("com")); diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 727500789e..543be7479e 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -447,6 +447,11 @@ drvsel.style.display = "none"; // hide by default drvsel.style.color = "#fff"; // reset color } + let bsfsel = gId("bsfsel"+n); + if (bsfsel) { + bsfsel.style.display = "none"; + bsfsel.style.color = "#fff"; + } s.style.color = "#fff"; // reset if (isDig(t) && !isD2P(t)) { @@ -459,6 +464,10 @@ if (!isBCok(n, usage)) drvsel.style.color = "red"; else drvsel.style.color = "#fff"; } } + // Show bus speed selector when advanced settings enabled + if (bsfsel) { + if (d.Sf.AS.checked) bsfsel.style.display = "inline"; + } } }); updateTypeDropdowns(); // update type dropdowns to disable unavailable digital/analog types (I2S/RMT bus count may have changed due to memory usage change) @@ -575,6 +584,13 @@ +
Host: .local

Reversed:

Skip first LEDs:
@@ -765,6 +781,7 @@ d.getElementsByName("AW"+i)[0].value = v.rgbwm; d.getElementsByName("WO"+i)[0].value = (v.order>>4) & 0x0F; d.getElementsByName("SP"+i)[0].value = v.freq; + d.getElementsByName("SF"+i)[0].value = v.bsf | 100; d.getElementsByName("LA"+i)[0].value = v.ledma; d.getElementsByName("MA"+i)[0].value = v.maxpwr; }); diff --git a/wled00/set.cpp b/wled00/set.cpp index 3c6c72b7b3..e10fb5dbee 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -207,6 +207,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //channel swap char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) + char sf[4] = "SF"; sf[2] = offset+s; sf[3] = 0; //bus speed factor (Fast/Default/Slow) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA char ld[4] = "LD"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1) @@ -265,7 +266,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text); + uint8_t bsf = request->hasArg(sf) ? (uint8_t)request->arg(sf).toInt() : 100; + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text, (uint8_t)bsf); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 5b5b785a2f..3b79564aef 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -179,7 +179,7 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, break; #endif -#ifdef WLEDPB_SPI_SUPPORT +#ifdef WLEDPB_PARALLEL_SPI_SUPPORT case BusType::SPI: bus = new ParallelSpiBus(pin, timing, order); break; @@ -211,7 +211,7 @@ BusType getRecommendedBusType() { return BusType::LCD; // S3 has LCD support #elif defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) return BusType::I2S; // Original and S2 have I2S parallel -#elif defined(WLEDPB_SPI_SUPPORT) +#elif defined(WLEDPB_PARALLEL_SPI_SUPPORT) return BusType::SPI; // C3 uses SPI quad mode */ #if defined(WLEDPB_ESP8266) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 7361070199..ced2ec2e2d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -72,16 +72,6 @@ by @dedehai, 2026 #endif #include "WLEDpixelBus_Timings.h" -#include "WLEDpixelBus_SPI.h" - -#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) -#include "WLEDpixelBus_RMT.h" -#include "WLEDpixelBus_I2S.h" -#include "WLEDpixelBus_LCD.h" -#include "WLEDpixelBus_ParallelSpi.h" -#elif defined(WLEDPB_ESP8266) -#include "WLEDpixelBus_ESP8266.h" -#endif namespace WLEDpixelBus { @@ -444,3 +434,13 @@ static inline size_t estimateMemory(BusType type, uint16_t numPixels, uint8_t ch } // namespace WLEDpixelBus +#include "WLEDpixelBus_SPI.h" + +#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) +#include "WLEDpixelBus_RMT.h" +#include "WLEDpixelBus_I2S.h" +#include "WLEDpixelBus_LCD.h" +#include "WLEDpixelBus_ParallelSpi.h" +#elif defined(WLEDPB_ESP8266) +#include "WLEDpixelBus_ESP8266.h" +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 8dc9611e6c..894d8b1e9d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -90,6 +90,8 @@ inline uint32_t calcClockDiv(const LedTiming& timing, uint8_t cadenceSteps, uint * @param cadenceSteps Number of cadence steps * @return Number of zero-bytes needed for reset period */ + +//TODO: currently unused, check if this is useful, remove otherwise. inline uint32_t calcResetBytes(const LedTiming& timing, uint8_t cadenceSteps) { uint32_t bitPeriodNs = timing.bitPeriod(cadenceSteps); uint32_t clockPeriodNs = bitPeriodNs / cadenceSteps; @@ -97,4 +99,18 @@ inline uint32_t calcResetBytes(const LedTiming& timing, uint8_t cadenceSteps) { return timing.reset_us * bytesPerUs; } +/** + * Scale LED timing parameters by a floating point factor (percent expressed as factor, e.g. 1.2 for +20%) + * Only t* timings (ns) are scaled; reset_us is preserved. + */ +inline LedTiming scaleTiming(const LedTiming& timing, float factor) { + auto s = [&](uint32_t v)->uint16_t { + uint32_t r = (uint32_t)(v * factor + 0.5f); + if (r < 1) r = 1; + if (r > 0xFFFF) r = 0xFFFF; + return (uint16_t)r; + }; + return LedTiming(s(timing.t0h_ns), s(timing.t0l_ns), s(timing.t1h_ns), s(timing.t1l_ns), timing.reset_us); +} + } // namespace WLEDpixelBus From b224bff468ed7641ce71d5bb794b9e8239a5b161 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 16 Mar 2026 18:59:28 +0100 Subject: [PATCH 047/173] fix UI bus timing a little, RMT still not working with custom timing --- wled00/data/settings_leds.htm | 66 ++++++++++++++++++++++-- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 5 +- wled00/xml.cpp | 2 + 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 543be7479e..5e1028198a 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -50,6 +50,17 @@ d.Sf.addEventListener("submit", trySubmit); if (d.um_p[0]==-1) d.um_p.shift(); pinDropdowns(); + // Populate per-bus Bus Speed Factor (bsf) from server config if present + try { + if (d && d.hw && d.hw.led && Array.isArray(d.hw.led.ins)) { + d.hw.led.ins.forEach((v,i)=>{ + let el = d.getElementsByName("SF"+i)[0]; + if (el) el.value = (typeof v.bsf !== 'undefined') ? (v.bsf | 100) : 100; + }); + } + } catch(e) {} + // initialize UI after populating values + UI(); }); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/leds'); } @@ -151,6 +162,7 @@ if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it) if (d.Sf.checkValidity()) { d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{s.disabled=false;}); // just in case + d.Sf.querySelectorAll("#mLC select[name^=SF]").forEach((s)=>{s.disabled=false;}); // ensure SF values are submitted even if some are disabled d.Sf.submit(); //https://stackoverflow.com/q/37323914 } } @@ -471,6 +483,8 @@ } }); updateTypeDropdowns(); // update type dropdowns to disable unavailable digital/analog types (I2S/RMT bus count may have changed due to memory usage change) + // enforce I2S bus speed selector rules (only first I2S bus editable; others mirror/disabled) + syncI2SBusSpeedSelectors(); // note: do not remvoe this second call to updateTypeDropdowns() as it also updates the available LED types based on the current bus configuration, not just the driver options // Show channel usage warning @@ -585,10 +599,14 @@
Host: .local
@@ -1029,8 +1047,46 @@ } } }); - } - + } + + // Ensure I2S bus speed selectors are synchronized: only the first I2S bus is editable + function syncI2SBusSpeedSelectors() { + try { + // First, enable all SF selects by default (re-enable any that stopped being I2S) + d.Sf.querySelectorAll("#mLC select[name^=SF]").forEach(s => { s.disabled = false; s.style.opacity = 1; }); + let i2sIndices = []; + d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach(sel => { + let n = sel.name.substring(2,3); + let t = parseInt(sel.value); + let drv = d.Sf["LD"+n]?.value | 0; + if (isDig(t) && !isD2P(t) && drv === 1) i2sIndices.push(n); + }); + if (i2sIndices.length === 0) return; + i2sIndices.sort((a,b)=> toNum(a) - toNum(b)); + const first = i2sIndices[0]; + for (const n of i2sIndices) { + let sf = d.getElementsByName("SF"+n)[0]; + if (!sf) continue; + if (n === first) { + sf.disabled = false; + sf.style.opacity = 1; + } else { + sf.disabled = true; + sf.style.opacity = 0.6; + let fv = d.getElementsByName("SF"+first)[0]; + if (fv && sf.value !== fv.value) sf.value = fv.value; + } + } + let firstEl = d.getElementsByName("SF"+first)[0]; + if (firstEl) firstEl.onchange = function() { + let v = this.value; + for (const n of i2sIndices) if (n !== first) { + let sf = d.getElementsByName("SF"+n)[0]; if (sf) sf.value = v; + } + }; + } catch(e) {} + } +
diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 3b79564aef..5ee26b6d15 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -146,12 +146,11 @@ ColorEncoder::ColorEncoder(ColorOrder order) : _order(order), _numChannels(getCh // Bus Factory Implementation //============================================================================== -IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, - ColorOrder order, size_t bufferSize, int8_t channel) { +IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, ColorOrder order, size_t bufferSize, int8_t channel) { Serial.printf("[WPB] createBus type=%u pin=%d bufSize=%u ch=%d\n", (unsigned)type, pin, bufferSize, channel); if (type == BusType::Auto) { - type = getRecommendedBusType(); // TODO: when is "auto" used? should auto default to RMT? it currently defaults to I2S + type = getRecommendedBusType(); // TODO: when is "auto" used? should auto default to RMT? -> uses RMT for now but check if this fallback is needed or even useful Serial.printf("[WPB] Auto resolved to type=%u\n", (unsigned)type); } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index dceebbdf09..9a9df5fd15 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -361,6 +361,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) @@ -402,6 +403,7 @@ 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()); From 4b8a501f232b66e6f2f58a6e1edabec837e11cef Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 16 Mar 2026 20:50:04 +0100 Subject: [PATCH 048/173] some improvements on parallel SPI, no more glitches but it can still lock up... --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 3 +- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 101 ++++++++++-------- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 2 +- 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index ac78d398e9..020e66b441 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -319,7 +319,8 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { #if defined(WLEDPB_ESP32) sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; #elif defined(WLEDPB_ESP32S2) - sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX + //sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX + sigIdx = I2S0O_DATA_OUT8_IDX; // !!! just a test to see if this works #else sigIdx = I2S0O_DATA_OUT0_IDX; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 48bcc6e6ca..809a0af100 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -108,13 +108,20 @@ void SpiBusContext::forceIdle() { isSending = false; // TODO: integrate this into driver, i.e. non global _stagedMask = 0; + // disconnect pins from SPI and set low + for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { + if (_channels[i].active && _channels[i].pin >= 0) { + gpio_matrix_out(_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); + gpio_set_level((gpio_num_t)_channels[i].pin, 0); + } + } spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); - // Stop DMA immediately + // Stop the DMA gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // disable interrupt + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); // reset DMA channel if (_hw) { _hw->cmd.usr = 0; // stop SPI user transfer (will output a fast clock, so detach pins from spi before) @@ -179,23 +186,23 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { ctx->_hw->dma_int_clr.val = raw; // Clear all SPI interrupt flags if (raw & SPI_TRANS_DONE_INT_ST) { - txdone = true; // transfer finished - // Serial.print("b"); + txdone = true; // transfer finished + // digitalWrite(0, LOW);//!!! + // Serial.print("b"); } else { - // Serial.println(raw, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST - // this may be some SPI error, stop SPI and DMA to prevent lockup - // detach pins from spi and force low to prevent any output when cmd.user is stopped (which outputs a fast pattern) - for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { - if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { - gpio_matrix_out(ctx->_channels[i].pin, SIG_GPIO_OUT_IDX, false, false); - gpio_set_level((gpio_num_t)ctx->_channels[i].pin, 0); - } - } - //ctx->_hw->cmd.usr = 0; // Stop SPI -> this cauuses a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) - //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks - ctx->forceIdle(); - txdone = true; // still mark it as done + // Serial.println(raw, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST + // this may be some SPI error, stop SPI and DMA to prevent lockup + // detach pins from spi and force low to prevent any output when cmd.user is stopped (which outputs a fast pattern) + + //ctx->_hw->cmd.usr = 0; // Stop SPI -> this causes a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) + //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks + //ctx->forceIdle(); // -> seems to cause the glitches + //txdone = true; // still mark it as done + // error, just mark it as done (might not be the best idea... but an attempt to prevent deadlocks)) + txdone = true; + ctx->_sending = false; + isSending = false; } @@ -206,32 +213,34 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; gdma_dev_t* dma = &GDMA; + // Serial.print("*"); // toggle gpio0 // digitalWrite(0, HIGH); + if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + //Clear interrupt immediately + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; uint8_t completedBuf = ctx->_currentBuffer; ctx->encodeSpiChunk(completedBuf); // if (isSending) { - // CRITICAL: Give ownership of the descriptor back to DMA so it can keep feeding SPI - // ctx->_dmaDesc[completedBuf].size = WLEDPB_SPI_DMA_BUFFER_SIZE; - // ctx->_dmaDesc[completedBuf].length = WLEDPB_SPI_DMA_BUFFER_SIZE; - //ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? - //ctx->_dmaDesc[completedBuf].owner = 1; + // Give ownership of the descriptor back to DMA so it can keep feeding SPI + ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? + ctx->_dmaDesc[completedBuf].owner = 1; // give ownership back to DMA: it works without this but just make sure its not passed back to CPU // } ctx->_currentBuffer = completedBuf ? 0 : 1; // toggle DMA buffer } // digitalWrite(0, LOW); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel +dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel } bool SpiBusContext::init(const LedTiming& timing) { if (_initialized) return true; - //pinMode(0, OUTPUT); - //digitalWrite(0, LOW); //!!! + pinMode(0, OUTPUT); + digitalWrite(0, LOW); //!!! // Allocate DMA buffers for (int i = 0; i < 2; i++) { @@ -319,9 +328,7 @@ bool SpiBusContext::init(const LedTiming& timing) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, - gdmaISR, this, &_isrHandle); + esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3, gdmaISR, this, &_isrHandle); if (err != ESP_OK) { Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); deinit(); @@ -332,9 +339,7 @@ bool SpiBusContext::init(const LedTiming& timing) { _hw->dma_int_clr.val = 0xFFFFFFFF; _hw->dma_int_ena.trans_done = 1; _hw->dma_int_ena.outfifo_empty_err = 1; - err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, - spiISR, this, &_spiIsrHandle); + err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3, spiISR, this, &_spiIsrHandle); if (err != ESP_OK) { Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); deinit(); @@ -431,10 +436,8 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ if (len > _numBytes) _numBytes = len; // update longest bus length } - bool SpiBusContext::startTransmit() { - if (_sending) return false; - if (_channelCount == 0) return false; + if (_sending || _channelCount == 0) return false; if (_stagedMask != _channelMask) return false; // not all channels ready yet _stagedMask = 0; @@ -453,7 +456,7 @@ bool SpiBusContext::startTransmit() { // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte // Plus reset: extra zero bits at the end - uint32_t resetBits = 1024;//5000; // ~300us reset at ~2.6MHz TODO: calculate from actual reset pulse, make sure its 4 byte aligned (just in case) + uint32_t resetBits = 0; //TODO: works better with this set to 0, find a better way for the reset pulse //4096;//5000; // ~300us reset at ~2.6MHz TODO: calculate from actual reset pulse, make sure its 4 byte aligned (just in case) uint32_t totalBits = (_numBytes * 16 * 8) + resetBits; if (totalBits > 262143) totalBits = 262143; // SPI max 18 bits for length register @@ -466,15 +469,16 @@ bool SpiBusContext::startTransmit() { encodeSpiChunk(0); encodeSpiChunk(1); - _dmaDesc[0].owner = 1; // make sure the DMA owns the buffer + _dmaDesc[0].owner = 1; // make sure the DMA owns the descriptor _dmaDesc[1].owner = 1; - //digitalWrite(0, LOW);//!!! + + digitalWrite(0, HIGH);//!!! spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); - spi_ll_dma_tx_enable(_hw, true); - spi_ll_apply_config(_hw); + //spi_ll_dma_tx_enable(_hw, true); + //spi_ll_apply_config(_hw); - // re-attach pins to SPI signals + // re-attach pins to SPI signals for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { if (_channels[i].active && _channels[i].pin >= 0) { pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], false, false); // TODO: should this trick also be used to force outputs low during reset pulse? @@ -486,10 +490,14 @@ bool SpiBusContext::startTransmit() { gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, 0); gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - delayMicroseconds(50); // Short delay to ensure DMA has started before SPI: if DMA did not start, SPI will immediately stall with "outfifo_empty_err", short delay makes it less prone (this may be yet another IDF V4 bug) + + spi_ll_apply_config(_hw); // apply SPI config AFTER starting DMA to make sure they are in sync + + // Short hardware handshake sync + delayMicroseconds(20); // Short delay to ensure DMA has time to fill SPI buffer: if DMA did not start, SPI will immediately stall with "outfifo_empty_err", short delay makes it less prone (this may be yet another IDF V4 bug) // Serial.print("a"); dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - //dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; _hasStarted = true; spi_ll_user_start(_hw); // start SPI user transfer // TODO: there are still some glitches/stalls due to tx_fifo underruns but it is unclear why, it may even be an IDF V4 bug (I read about some timing issue with buffer handover or a missing nop) @@ -586,7 +594,7 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; // Wait for previous transmission - /* +/* uint32_t startWait = millis(); while (!_ctx->isIdle()) { if (millis() - startWait > 500) { @@ -597,15 +605,14 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP }*/ // Wait for SPI to finish any remaining transfer - /* - startWait = millis(); +uint32_t startWait = millis(); while (!_ctx->isSpiDone()) { if (millis() - startWait > 500) { _ctx->forceIdle(); return false; } vTaskDelay(1); - }*/ + } if (!allocateBuffer(_numPixels)) return false; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index 5e1fc5a304..aade34beb5 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -10,7 +10,7 @@ namespace WLEDpixelBus { #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines -#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (1k per buffer means about 0.5ms interrupt intervals) +#define WLEDPB_SPI_DMA_BUFFER_SIZE 2048 // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (2k per buffer means about 1ms interrupt intervals with 4 step cadence) #define WLEDPB_SPI_GDMA_CHANNEL 0 class ParallelSpiBus; From 2d1ebac177a17dc4978c7d328ed0a9bd3a0f7cef Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 16 Mar 2026 21:12:52 +0100 Subject: [PATCH 049/173] finally fix I2S for S2: enable correct clock --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 020e66b441..9891716382 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -220,7 +220,15 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // Set clock (with fractional divider for accurate timing) _i2sDev->clkm_conf.val = 0; - _i2sDev->clkm_conf.clk_en = 1; + + #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; From b2fbf088985ab6ae4712ebe1d353f7f797f00a49 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 17 Mar 2026 13:20:43 +0100 Subject: [PATCH 050/173] add comments, todos and ifdef for RMT autochannel --- wled00/bus_wrapper.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 2854f3f445..ce6116b03b 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -201,13 +201,13 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, #else if (_2PchannelsAssigned == 1) isHSPI = true; #endif - return new WLEDpixelBus::SpiBus(pins[0], pins[1], timing, finalOrder, isHSPI); + return new WLEDpixelBus::SpiBus(pins[0], pins[1], timing, finalOrder, isHSPI); // TODO: move this into createbus function? } - auto btype = WLEDpixelBus::BusType::Auto; - + auto btype = WLEDpixelBus::BusType::Auto; // TODO: there is the get preferred bus type function but below we set default to RMT + #ifdef ESP8266 - // uint8_t offset = pins[0] - 1; + // uint8_t offset = pins[0] - 1; // if (pins[0] == 1 || pins[0] == 2) btype = WLEDpixelBus::BusType::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled TODO: there is a bug, TX1 only works after saving, not after reboot, needs investigation, use bitbanging for now //else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; // DMA method uses a lot of RAM! //else btype = WLEDpixelBus::BusType::BitBang; @@ -217,11 +217,11 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, case 0: btype = WLEDpixelBus::BusType::RMT; break; case 1: #if defined(CONFIG_IDF_TARGET_ESP32S3) - btype = WLEDpixelBus::BusType::LCD; + btype = WLEDpixelBus::BusType::LCD; // S3 has LCD peripheral with 16 channels, very similar to I2S #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) btype = WLEDpixelBus::BusType::I2S; #elif defined(CONFIG_IDF_TARGET_ESP32C3) - btype = WLEDpixelBus::BusType::SPI; + btype = WLEDpixelBus::BusType::SPI; // parallel SPI #else btype = WLEDpixelBus::BusType::RMT; #endif @@ -230,12 +230,15 @@ static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, } #endif - int8_t rmtCh = -1; // -1 means auto select by RMT driver + int8_t rmtCh = -1; // -1 means auto select by RMT driver and optimize memory block use, if RMT RX is needed, define RMT_USE_SINGLE_MEM_BLOCK #ifndef ESP8266 if (btype == WLEDpixelBus::BusType::RMT) { if (_rmtChannel < WLED_MAX_RMT_CHANNELS) { - //rmtCh = _rmtChannel++; // assign channels in order, do not use auto-channel function (this uses 1 memory block per channel allowing RX RMT channels to be used as well) + #ifdef RMT_USE_SINGLE_MEM_BLOCK + rmtCh = _rmtChannel++; // assign channels in order, do not use auto-channel function (this uses 1 memory block per channel allowing RX RMT channels to be used as well) + #else _rmtChannel++; // increment channel count for tracking, but use auto-channel to optimize memory block allocation + #endif } else { return nullptr; } From 2dded7f26749b097391cdc9b8af7a49ace578b7b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 17 Mar 2026 18:11:54 +0100 Subject: [PATCH 051/173] adding per RMT bus timing, still glitchy but works... --- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 170 +++++++++++-------- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 34 +++- 2 files changed, 133 insertions(+), 71 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 809036d6ed..dafa8fb428 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -8,12 +8,34 @@ namespace WLEDpixelBus { // RMT Bus Implementation //============================================================================== -// Context for RMT translate callback - must be in DRAM for IRAM ISR access -static DRAM_ATTR struct { - uint32_t bit0; - uint32_t bit1; - uint16_t resetDuration; -} s_rmtCtx; +// Per-channel context table - stored in DRAM for ISR access +DRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[8] = {}; + +// Explicit IRAM tranlator callback wrappers for each channel (ensures the function is placed in IRAM which is dropped when using templates) +// TODO: only define the ones actually needed based on available RMT channels, is there an IDF define for this? +void IRAM_ATTR RmtBus::translator_ch0(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(0, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch1(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(1, s, d, ss, w, ts, in); } +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 +void IRAM_ATTR RmtBus::translator_ch2(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(2, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch3(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(3, s, d, ss, w, ts, in); } +#endif +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 +void IRAM_ATTR RmtBus::translator_ch4(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(4, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch5(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(5, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch6(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(6, s, d, ss, w, ts, in); } +void IRAM_ATTR RmtBus::translator_ch7(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(7, s, d, ss, w, ts, in); } +#endif +// Jump table stored in DRAM so ISR code can quickly find the correct wrapper +DRAM_ATTR const sample_to_rmt_t RmtBus::s_callbacks[8] = { + RmtBus::translator_ch0, RmtBus::translator_ch1 +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 + ,RmtBus::translator_ch2, RmtBus::translator_ch3 +#endif +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 + ,RmtBus::translator_ch4, RmtBus::translator_ch5, + RmtBus::translator_ch6, RmtBus::translator_ch7 +#endif +}; // Static auto-channel counter for RmtBus uint8_t RmtBus::s_expectedChannels = 1; @@ -57,6 +79,8 @@ void RmtBus::updateRmtTiming() { uint16_t t1h = nsToTicks(_timing.t1h_ns); uint16_t t1l = nsToTicks(_timing.t1l_ns); + Serial.printf("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); + rmt_item32_t bit0, bit1; if (_inverted) { @@ -74,6 +98,13 @@ void RmtBus::updateRmtTiming() { _rmtBit0 = bit0.val; _rmtBit1 = bit1.val; _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); + + // If already initialized, update the shared DRAM context for this channel + if (_initialized) { + s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; + s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; + s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; + } } bool RmtBus::begin() { @@ -134,6 +165,11 @@ bool RmtBus::begin() { updateRmtTiming(); + // Store initial timing into the shared DRAM context for this channel + s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; + s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; + s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; + rmt_config_t config = {}; config.rmt_mode = RMT_MODE_TX; @@ -153,27 +189,13 @@ bool RmtBus::begin() { return false; } - // Prioritize RMT over I2S/SPI DMA interrupts (which use LEVEL1) to prevent starvation. - // Try LEVEL3 first, fallback to LEVEL2, then LEVEL1. - int flags = ESP_INTR_FLAG_IRAM; -#ifdef ESP_INTR_FLAG_LEVEL3 - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL3); - if (err != ESP_OK) -#endif - { -#ifdef ESP_INTR_FLAG_LEVEL2 - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL2); - if (err != ESP_OK) -#endif - { - err = rmt_driver_install(_rmtChannel, 0, flags | ESP_INTR_FLAG_LEVEL1); - } - } + // install driver at high priority TODO: need to raise priority just like in RMT high priority driver + err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)); if (err != ESP_OK) { return false; } - // Register hack for memory blocks normally assigned to RX + // Register hack for memory blocks normally assigned to RX TODO: is this also needed for S2 / classic ESP32? #if defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) #if defined(WLEDPB_ESP32S3) for (int i = 4; i < 8; i++) { @@ -186,7 +208,7 @@ bool RmtBus::begin() { #endif #endif - err = rmt_translator_init(_rmtChannel, translateCB); + err = rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); if (err != ESP_OK) { rmt_driver_uninstall(_rmtChannel); return false; @@ -259,10 +281,10 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc dst += numCh; } - // Update context for ISR - s_rmtCtx.bit0 = _rmtBit0; - s_rmtCtx.bit1 = _rmtBit1; - s_rmtCtx.resetDuration = _rmtResetTicks; + // Update per-channel context for translator (ensure latest timings are visible to ISR) + s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; + s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; + s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; // Start transmission size_t dataLen = numPixels * numCh; @@ -284,6 +306,7 @@ void RmtBus::waitComplete() { } void RmtBus::setTiming(const LedTiming& timing) { + Serial.printf("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); _timing = timing; if (_initialized) { updateRmtTiming(); @@ -294,9 +317,9 @@ void RmtBus::setColorOrder(ColorOrder order) { _order = order; } -void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num) { +void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num) { if (src == nullptr || dest == nullptr) { *translated_size = 0; *item_num = 0; @@ -305,46 +328,59 @@ void IRAM_ATTR RmtBus::translateCB(const void* src, rmt_item32_t* dest, const uint8_t* psrc = (const uint8_t*)src; rmt_item32_t* pdest = dest; - size_t size = 0; - size_t num = 0; - - uint32_t bit0 = s_rmtCtx.bit0; - uint32_t bit1 = s_rmtCtx.bit1; - uint16_t resetDuration = s_rmtCtx.resetDuration; - - // Each byte produces 8 RMT items - for (;;) - { - uint8_t data = *psrc; - - for (uint8_t bit = 0; bit < 8; bit++) - { - pdest->val = (data & 0x80) ? bit1 : bit0; - pdest++; - data <<= 1; - } - num += 8; - size++; - - // If this is the last byte, extend the last bit's LOW duration - // to include the full reset signal length - if (size >= src_size) - { - pdest--; - pdest->duration1 = resetDuration; - break; - } + + // Cache instance timings in registers for maximum ISR speed + const uint32_t bit0 = s_contexts[channel].bit0; + const uint32_t bit1 = s_contexts[channel].bit1; + const uint16_t resetDuration = s_contexts[channel].resetDuration; + + // Calculate how many full bytes we can translate based on RMT buffer space (wanted_num) + size_t bytes_to_process = src_size; + size_t items_limit = wanted_num / 8; + if (bytes_to_process > items_limit) bytes_to_process = items_limit; + + // Process all but the last byte in a tight loop without reset checks -> no branching in the loop for maximum speed + size_t i = 0; + size_t fast_limit = (bytes_to_process > 0) ? (bytes_to_process - 1) : 0; + + for (; i < fast_limit; i++) { + const uint8_t data = psrc[i]; + + // using loop unrolling makes this faster avoiding bit shifts, loop overhead and lets the compiler optimize more + pdest[0].val = (data & 0x80) ? bit1 : bit0; + pdest[1].val = (data & 0x40) ? bit1 : bit0; + pdest[2].val = (data & 0x20) ? bit1 : bit0; + pdest[3].val = (data & 0x10) ? bit1 : bit0; + pdest[4].val = (data & 0x08) ? bit1 : bit0; + pdest[5].val = (data & 0x04) ? bit1 : bit0; + pdest[6].val = (data & 0x02) ? bit1 : bit0; + pdest[7].val = (data & 0x01) ? bit1 : bit0; + + pdest += 8; + } - if (num >= wanted_num) - { - break; + // process the final byte separately to handle the reset duration (otherwise a check for "last byte" is needed in the loop above) + // i.e. prioritize speed over code simplicity as this ISR runs per RMT channel and at very high frequency (hundreds of times per second per channel) + if (i < src_size && (i * 8) < wanted_num) { + const uint8_t data = psrc[i]; + pdest[0].val = (data & 0x80) ? bit1 : bit0; + pdest[1].val = (data & 0x40) ? bit1 : bit0; + pdest[2].val = (data & 0x20) ? bit1 : bit0; + pdest[3].val = (data & 0x10) ? bit1 : bit0; + pdest[4].val = (data & 0x08) ? bit1 : bit0; + pdest[5].val = (data & 0x04) ? bit1 : bit0; + pdest[6].val = (data & 0x02) ? bit1 : bit0; + pdest[7].val = (data & 0x01) ? bit1 : bit0; + + i++; + // If this byte is the end of the source data, inject the Reset Signal + if (i == src_size) { + pdest[7].duration1 = resetDuration; } - - psrc++; } - *translated_size = size; - *item_num = num; + *translated_size = i; + *item_num = i * 8; } } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 05903c3f05..1b1a551000 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -71,10 +71,36 @@ class RmtBus : public IBus { void updateRmtTiming(); bool allocateBuffer(uint16_t numPixels); - // Static translate callback - static void IRAM_ATTR translateCB(const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num); + // Per-channel translator context and helpers + struct RmtContext { + uint32_t bit0; + uint32_t bit1; + uint16_t resetDuration; + }; + + // Static lookup table for ISR speed (max 8 channels) + static RmtContext s_contexts[8]; + + // Explicit wrappers: implemented in .cpp file to ensure they are placed in IRAM + static void IRAM_ATTR translator_ch0(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch1(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 + static void IRAM_ATTR translator_ch2(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch3(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + #endif + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 + static void IRAM_ATTR translator_ch4(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch5(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch6(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + static void IRAM_ATTR translator_ch7(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); + #endif + // Actual translator implementation (defined in .cpp) + static void IRAM_ATTR translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, + size_t src_size, size_t wanted_num, + size_t* translated_size, size_t* item_num); + + // Jump table of callbacks (defined in .cpp). Use 8 entries to match max RMT channels. + static const sample_to_rmt_t s_callbacks[8]; }; } // namespace WLEDpixelBus From 9aaa7c4ed8bfc4af481558a3a78a4463db21f690 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 18 Mar 2026 19:19:23 +0100 Subject: [PATCH 052/173] bugfix in I2S for S2 (remove test line), some refactoring --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 96 ++++++++------------ wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 13 ++- 2 files changed, 43 insertions(+), 66 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 9891716382..29ef62d0a0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -80,7 +80,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { } memset(_dmaBuffer[i], 0, _bufferSize); - _dmaDesc[i] = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t), MALLOC_CAP_DMA); + _dmaDesc[i] = (lldesc_t*)heap_caps_aligned_alloc(4, sizeof(lldesc_t), MALLOC_CAP_DMA); if (!_dmaDesc[i]) { Serial.println("I2S DMA desc alloc failed"); deinit(); @@ -97,7 +97,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _dmaDesc[i]->sosf = 0; _dmaDesc[i]->owner = 1; } - _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; + _dmaDesc[0]->qe.stqe_next = _dmaDesc[1]; // create circular list _dmaDesc[1]->qe.stqe_next = _dmaDesc[0]; // Enable I2S peripheral @@ -211,22 +211,20 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _clockDiv = clkmInteger; - Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", - bitPeriodNs, clkmInteger, divB, divA, bckDiv); + Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", bitPeriodNs, clkmInteger, divB, divA, bckDiv); double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; - Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", - actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); + Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); // 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 + _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; + _i2sDev->clkm_conf.clk_en = 1; + _i2sDev->clkm_conf.clka_en = 0; #endif _i2sDev->clkm_conf.clkm_div_a = divA; @@ -248,15 +246,13 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // Install ISR int intSource; -#if defined(WLEDPB_ESP32) + #if defined(WLEDPB_ESP32) intSource = (_busNum == 0) ? ETS_I2S0_INTR_SOURCE : ETS_I2S1_INTR_SOURCE; -#else + #else intSource = ETS_I2S0_INTR_SOURCE; -#endif + #endif - esp_err_t err = esp_intr_alloc(intSource, - ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, - dmaISR, this, &_isrHandle); + esp_err_t err = esp_intr_alloc(intSource, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2, dmaISR, this, &_isrHandle); if (err != ESP_OK) { Serial.printf("I2S ISR alloc failed: %d", err); deinit(); @@ -324,14 +320,13 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus) { // Route I2S output to GPIO int sigIdx; -#if defined(WLEDPB_ESP32) + #if defined(WLEDPB_ESP32) sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; -#elif defined(WLEDPB_ESP32S2) - //sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX - sigIdx = I2S0O_DATA_OUT8_IDX; // !!! just a test to see if this works -#else + #elif defined(WLEDPB_ESP32S2) + sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes, note: in 16bit mode, it starts at I2S0O_DATA_OUT8_IDX, in 24bit mode at I2S0O_DATA_OUT0_IDX + #else sigIdx = I2S0O_DATA_OUT0_IDX; -#endif + #endif sigIdx += idx; gpio_matrix_out(pin, sigIdx, false, false); @@ -405,44 +400,27 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { uint8_t* p = dest + pos; -#if defined(WLEDPB_ESP32S2) + #if defined(WLEDPB_ESP32S2) // ESP32-S2 does NOT swap half-words (memory layout [step0, step1, step2, step3]) - // bit 7 - p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 6 - p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 5 - p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 4 - p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 3 - p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 2 - p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 1 - p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; - // bit 0 - p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; -#else - // Half-word swapped: memory layout [step2, step3, step0, step1] - // bit 7 - p[2] |= chMask; if (srcByte & 0x80) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 6 - p[2] |= chMask; if (srcByte & 0x40) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 5 - p[2] |= chMask; if (srcByte & 0x20) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 4 - p[2] |= chMask; if (srcByte & 0x10) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 3 - p[2] |= chMask; if (srcByte & 0x08) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 2 - p[2] |= chMask; if (srcByte & 0x04) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 1 - p[2] |= chMask; if (srcByte & 0x02) { p[3] |= chMask; p[0] |= chMask; } p += 4; - // bit 0 - p[2] |= chMask; if (srcByte & 0x01) { p[3] |= chMask; p[0] |= chMask; } p += 4; -#endif - + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 7 + p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 6 + p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 5 + p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 4 + p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 3 + p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 2 + p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 1 + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; p[2] |= chMask; } p += 4; // bit 0 + #else + // ESP32 classic: Half-word swapped: memory layout [step2, step3, step0, step1] + p[2] |= chMask; if (srcByte & 0x80) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 7 + p[2] |= chMask; if (srcByte & 0x40) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 6 + p[2] |= chMask; if (srcByte & 0x20) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 5 + p[2] |= chMask; if (srcByte & 0x10) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 4 + p[2] |= chMask; if (srcByte & 0x08) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 3 + p[2] |= chMask; if (srcByte & 0x04) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 2 + p[2] |= chMask; if (srcByte & 0x02) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 1 + p[2] |= chMask; if (srcByte & 0x01) { p[3] |= chMask; p[0] |= chMask; } p += 4; // bit 0 + #endif } if (!hasData) break; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index dafa8fb428..61a97c021e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -328,22 +328,21 @@ void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_i const uint8_t* psrc = (const uint8_t*)src; rmt_item32_t* pdest = dest; - + // Cache instance timings in registers for maximum ISR speed const uint32_t bit0 = s_contexts[channel].bit0; const uint32_t bit1 = s_contexts[channel].bit1; const uint16_t resetDuration = s_contexts[channel].resetDuration; // Calculate how many full bytes we can translate based on RMT buffer space (wanted_num) - size_t bytes_to_process = src_size; - size_t items_limit = wanted_num / 8; - if (bytes_to_process > items_limit) bytes_to_process = items_limit; + const uint32_t items_limit = wanted_num / 8; + const uint32_t bytes_to_process = (src_size > items_limit) ? items_limit : src_size; // Process all but the last byte in a tight loop without reset checks -> no branching in the loop for maximum speed - size_t i = 0; - size_t fast_limit = (bytes_to_process > 0) ? (bytes_to_process - 1) : 0; + const uint32_t fast_limit = (bytes_to_process > 0) ? (bytes_to_process - 1) : 0; - for (; i < fast_limit; i++) { + uint32_t i; + for (i = 0; i < fast_limit; i++) { const uint8_t data = psrc[i]; // using loop unrolling makes this faster avoiding bit shifts, loop overhead and lets the compiler optimize more From 9607705edb20e9402a644bf7becade4772cb9af5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 18 Mar 2026 20:03:32 +0100 Subject: [PATCH 053/173] improve RMT interrupt --- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 37 +++++--------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 61a97c021e..28d6188cca 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -327,10 +327,9 @@ void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_i } const uint8_t* psrc = (const uint8_t*)src; - rmt_item32_t* pdest = dest; // Cache instance timings in registers for maximum ISR speed - const uint32_t bit0 = s_contexts[channel].bit0; + const uint32_t bit0 = s_contexts[channel].bit0; const uint32_t bit1 = s_contexts[channel].bit1; const uint16_t resetDuration = s_contexts[channel].resetDuration; @@ -338,12 +337,9 @@ void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_i const uint32_t items_limit = wanted_num / 8; const uint32_t bytes_to_process = (src_size > items_limit) ? items_limit : src_size; - // Process all but the last byte in a tight loop without reset checks -> no branching in the loop for maximum speed - const uint32_t fast_limit = (bytes_to_process > 0) ? (bytes_to_process - 1) : 0; - - uint32_t i; - for (i = 0; i < fast_limit; i++) { + for (uint32_t i = 0; i < bytes_to_process; i++) { const uint8_t data = psrc[i]; + rmt_item32_t* pdest = &dest[i * 8]; // using loop unrolling makes this faster avoiding bit shifts, loop overhead and lets the compiler optimize more pdest[0].val = (data & 0x80) ? bit1 : bit0; @@ -354,32 +350,15 @@ void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_i pdest[5].val = (data & 0x04) ? bit1 : bit0; pdest[6].val = (data & 0x02) ? bit1 : bit0; pdest[7].val = (data & 0x01) ? bit1 : bit0; - - pdest += 8; } - // process the final byte separately to handle the reset duration (otherwise a check for "last byte" is needed in the loop above) - // i.e. prioritize speed over code simplicity as this ISR runs per RMT channel and at very high frequency (hundreds of times per second per channel) - if (i < src_size && (i * 8) < wanted_num) { - const uint8_t data = psrc[i]; - pdest[0].val = (data & 0x80) ? bit1 : bit0; - pdest[1].val = (data & 0x40) ? bit1 : bit0; - pdest[2].val = (data & 0x20) ? bit1 : bit0; - pdest[3].val = (data & 0x10) ? bit1 : bit0; - pdest[4].val = (data & 0x08) ? bit1 : bit0; - pdest[5].val = (data & 0x04) ? bit1 : bit0; - pdest[6].val = (data & 0x02) ? bit1 : bit0; - pdest[7].val = (data & 0x01) ? bit1 : bit0; - - i++; - // If this byte is the end of the source data, inject the Reset Signal - if (i == src_size) { - pdest[7].duration1 = resetDuration; - } + // If max_bytes == src_size, it means we've reached the end of the LED strip. + if (bytes_to_process == src_size) { + dest[(bytes_to_process * 8) - 1].duration1 = resetDuration; } - *translated_size = i; - *item_num = i * 8; + *translated_size = bytes_to_process; + *item_num = bytes_to_process * 8; } } // namespace WLEDpixelBus From f6b70914749aa576822da8281bded2cd0546d24e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 18 Mar 2026 20:45:31 +0100 Subject: [PATCH 054/173] integrating RMT high priority ISR driver by @willmmiles --- .../include/NeoEsp32RmtHIMethod.h | 469 ------------------ lib/NeoESP32RmtHI/library.json | 12 - .../src/WLEDpixelBus}/NeoEsp32RmtHI.S | 2 +- .../src/WLEDpixelBus}/NeoEsp32RmtHIMethod.cpp | 42 +- wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.h | 30 ++ wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 95 +++- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 7 +- 7 files changed, 123 insertions(+), 534 deletions(-) delete mode 100644 lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h delete mode 100644 lib/NeoESP32RmtHI/library.json rename {lib/NeoESP32RmtHI/src => wled00/src/WLEDpixelBus}/NeoEsp32RmtHI.S (99%) rename {lib/NeoESP32RmtHI/src => wled00/src/WLEDpixelBus}/NeoEsp32RmtHIMethod.cpp (92%) create mode 100644 wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.h diff --git a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h b/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h deleted file mode 100644 index 072f6c112d..0000000000 --- a/lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h +++ /dev/null @@ -1,469 +0,0 @@ -/*------------------------------------------------------------------------- -NeoPixel driver for ESP32 RMTs using High-priority Interrupt - -(NB. This cannot be mixed with the non-HI driver.) - -Written by Will M. Miles. - -I invest time and resources providing this open source code, -please support me by donating (see https://github.com/Makuna/NeoPixelBus) - -------------------------------------------------------------------------- -This file is part of the Makuna/NeoPixelBus library. - -NeoPixelBus is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as -published by the Free Software Foundation, either version 3 of -the License, or (at your option) any later version. - -NeoPixelBus is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with NeoPixel. If not, see -. --------------------------------------------------------------------------*/ - -#pragma once - -#if defined(ARDUINO_ARCH_ESP32) - -// Use the NeoEspRmtSpeed types from the driver-based implementation -#include - - -namespace NeoEsp32RmtHiMethodDriver { - // Install the driver for a specific channel, specifying timing properties - esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration); - - // Remove the driver on a specific channel - esp_err_t Uninstall(rmt_channel_t channel); - - // Write a buffer of data to a specific channel. - // Buffer reference is held until write completes. - esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size); - - // Wait until transaction is complete. - esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time); -}; - -template class NeoEsp32RmtHIMethodBase -{ -public: - typedef NeoNoSettings SettingsObject; - - NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize) : - _sizeData(pixelCount * elementSize + settingsSize), - _pin(pin) - { - construct(); - } - - NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize, NeoBusChannel channel) : - _sizeData(pixelCount* elementSize + settingsSize), - _pin(pin), - _channel(channel) - { - construct(); - } - - ~NeoEsp32RmtHIMethodBase() - { - // wait until the last send finishes before destructing everything - // arbitrary time out of 10 seconds - ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS)); - - ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Uninstall(_channel.RmtChannelNumber)); - - gpio_matrix_out(_pin, SIG_GPIO_OUT_IDX, false, false); - pinMode(_pin, INPUT); - - free(_dataEditing); - free(_dataSending); - } - - bool IsReadyToUpdate() const - { - return (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT_SILENT_TIMEOUT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 0))); - } - - void Initialize() - { - rmt_config_t config = {}; - - config.rmt_mode = RMT_MODE_TX; - config.channel = _channel.RmtChannelNumber; - config.gpio_num = static_cast(_pin); - config.mem_block_num = 1; - config.tx_config.loop_en = false; - - config.tx_config.idle_output_en = true; - config.tx_config.idle_level = T_SPEED::IdleLevel; - - config.tx_config.carrier_en = false; - config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - - config.clk_div = T_SPEED::RmtClockDivider; - - ESP_ERROR_CHECK(rmt_config(&config)); // Uses ESP library - ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Install(_channel.RmtChannelNumber, T_SPEED::RmtBit0, T_SPEED::RmtBit1, T_SPEED::RmtDurationReset)); - } - - void Update(bool maintainBufferConsistency) - { - // wait for not actively sending data - // this will time out at 10 seconds, an arbitrarily long period of time - // and do nothing if this happens - if (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS))) - { - // now start the RMT transmit with the editing buffer before we swap - ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::Write(_channel.RmtChannelNumber, _dataEditing, _sizeData)); - - if (maintainBufferConsistency) - { - // copy editing to sending, - // this maintains the contract that "colors present before will - // be the same after", otherwise GetPixelColor will be inconsistent - memcpy(_dataSending, _dataEditing, _sizeData); - } - - // swap so the user can modify without affecting the async operation - std::swap(_dataSending, _dataEditing); - } - } - - bool AlwaysUpdate() - { - // this method requires update to be called only if changes to buffer - return false; - } - - bool SwapBuffers() - { - std::swap(_dataSending, _dataEditing); - return true; - } - - uint8_t* getData() const - { - return _dataEditing; - }; - - size_t getDataSize() const - { - return _sizeData; - } - - void applySettings([[maybe_unused]] const SettingsObject& settings) - { - } - -private: - const size_t _sizeData; // Size of '_data*' buffers - const uint8_t _pin; // output pin number - const T_CHANNEL _channel; // holds instance for multi channel support - - // Holds data stream which include LED color values and other settings as needed - uint8_t* _dataEditing; // exposed for get and set - uint8_t* _dataSending; // used for async send using RMT - - - void construct() - { - _dataEditing = static_cast(malloc(_sizeData)); - // data cleared later in Begin() - - _dataSending = static_cast(malloc(_sizeData)); - // no need to initialize it, it gets overwritten on every send - } -}; - -// normal -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsMethod; -typedef NeoEsp32RmtHINWs2805Method NeoEsp32RmtHINWs2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsMethod; -typedef NeoEsp32RmtHI0Ws2805Method NeoEsp32RmtHI0Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsMethod; -typedef NeoEsp32RmtHI1Ws2805Method NeoEsp32RmtHI1Ws2814Method; - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsMethod; -typedef NeoEsp32RmtHI2Ws2805Method NeoEsp32RmtHI2Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsMethod; -typedef NeoEsp32RmtHI3Ws2805Method NeoEsp32RmtHI3Ws2814Method; - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsMethod; -typedef NeoEsp32RmtHI4Ws2805Method NeoEsp32RmtHI4Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsMethod; -typedef NeoEsp32RmtHI5Ws2805Method NeoEsp32RmtHI5Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsMethod; -typedef NeoEsp32RmtHI6Ws2805Method NeoEsp32RmtHI6Ws2814Method; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903Method; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsMethod; -typedef NeoEsp32RmtHI7Ws2805Method NeoEsp32RmtHI7Ws2814Method; - -#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) -#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) - -// inverted -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsInvertedMethod; -typedef NeoEsp32RmtHINWs2805InvertedMethod NeoEsp32RmtHINWs2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsInvertedMethod; -typedef NeoEsp32RmtHI0Ws2805InvertedMethod NeoEsp32RmtHI0Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsInvertedMethod; -typedef NeoEsp32RmtHI1Ws2805InvertedMethod NeoEsp32RmtHI1Ws2814InvertedMethod; - -#if !defined(CONFIG_IDF_TARGET_ESP32C3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsInvertedMethod; -typedef NeoEsp32RmtHI2Ws2805InvertedMethod NeoEsp32RmtHI2Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsInvertedMethod; -typedef NeoEsp32RmtHI3Ws2805InvertedMethod NeoEsp32RmtHI3Ws2814InvertedMethod; - -#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsInvertedMethod; -typedef NeoEsp32RmtHI4Ws2805InvertedMethod NeoEsp32RmtHI4Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsInvertedMethod; -typedef NeoEsp32RmtHI5Ws2805InvertedMethod NeoEsp32RmtHI5Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsInvertedMethod; -typedef NeoEsp32RmtHI6Ws2805InvertedMethod NeoEsp32RmtHI6Ws2814InvertedMethod; - -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903InvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsInvertedMethod; -typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsInvertedMethod; -typedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod; - -#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) -#endif // !defined(CONFIG_IDF_TARGET_ESP32C3) - -#endif diff --git a/lib/NeoESP32RmtHI/library.json b/lib/NeoESP32RmtHI/library.json deleted file mode 100644 index 0608e59e12..0000000000 --- a/lib/NeoESP32RmtHI/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "NeoESP32RmtHI", - "build": { "libArchive": false }, - "platforms": ["espressif32"], - "dependencies": [ - { - "owner": "makuna", - "name": "NeoPixelBus", - "version": "^2.8.3" - } - ] -} diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S b/wled00/src/WLEDpixelBus/NeoEsp32RmtHI.S similarity index 99% rename from lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S rename to wled00/src/WLEDpixelBus/NeoEsp32RmtHI.S index 0c60d2ebc9..126fd3023e 100644 --- a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S +++ b/wled00/src/WLEDpixelBus/NeoEsp32RmtHI.S @@ -2,7 +2,7 @@ * Bridges from a high-level interrupt to the C++ code. * * This code is largely derived from Espressif's 'hli_vector.S' Bluetooth ISR. - * + * implemented by @willmmiles */ #if defined(__XTENSA__) && defined(ESP32) && !defined(CONFIG_BTDM_CTRL_HLI) diff --git a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp b/wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.cpp similarity index 92% rename from lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp rename to wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.cpp index 8353201f08..953108e1ff 100644 --- a/lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp +++ b/wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.cpp @@ -1,37 +1,13 @@ /*------------------------------------------------------------------------- -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 -. +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 -------------------------------------------------------------------------*/ #include #if defined(ARDUINO_ARCH_ESP32) -#include +#include #include "esp_idf_version.h" #include "NeoEsp32RmtHIMethod.h" #include "soc/soc.h" @@ -309,8 +285,8 @@ 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]) { + 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); @@ -328,7 +304,7 @@ extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) { uint32_t v; switch(channel) { -#ifdef RMT_STATE_CH0 +#ifdef RMT_STATE_CH0 case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break; #endif #ifdef RMT_STATE_CH1 @@ -337,9 +313,9 @@ static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t stat #ifdef RMT_STATE_CH2 case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break; #endif -#ifdef RMT_STATE_CH3 +#ifdef RMT_STATE_CH3 case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break; -#endif +#endif #ifdef RMT_STATE_CH4 case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break; #endif @@ -504,4 +480,4 @@ esp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickTy return rv; } -#endif \ No newline at end of file +#endif diff --git a/wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.h b/wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.h new file mode 100644 index 0000000000..c6e4fd80c7 --- /dev/null +++ b/wled00/src/WLEDpixelBus/NeoEsp32RmtHIMethod.h @@ -0,0 +1,30 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - high priority RMT interrupts for ESP32 +by @willmmiles, 2026 +-------------------------------------------------------------------------*/ +#pragma once + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include "driver/rmt.h" +#include "freertos/FreeRTOS.h" +#include +#include + +namespace 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); +}; + +#endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 28d6188cca..635bf0904d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -51,6 +51,7 @@ RmtBus::RmtBus(int8_t pin, const LedTiming& timing, ColorOrder order, int8_t cha , _order(order) , _inverted(false) , _initialized(false) + , _usingRmtHi(false) , _rmtChannel(RMT_CHANNEL_0) , _rmtBit0(0) , _rmtBit1(0) @@ -66,7 +67,6 @@ RmtBus::~RmtBus() { void RmtBus::updateRmtTiming() { // RMT clock: 80MHz with div=2 -> 40MHz -> 25ns per tick - const uint8_t clockDiv = 2; const float tickNs = 25.0f; auto nsToTicks = [tickNs](uint16_t ns) -> uint16_t { @@ -99,11 +99,31 @@ void RmtBus::updateRmtTiming() { _rmtBit1 = bit1.val; _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); - // If already initialized, update the shared DRAM context for this channel + // If already initialized, update hardware state for this channel if (_initialized) { - s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; - s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; - s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; + // Update shared DRAM context used by the IDF translator (not needed for rmtHi) + if (!_usingRmtHi) { + s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; + s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; + s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; + } + // If using rmtHi, reinstall the driver with new timing templates + if (_usingRmtHi) { + // wait for any in-flight transfer, then reinstall the hi driver + NeoEsp32RmtHiMethodDriver::WaitForTxDone(_rmtChannel, 1000 / portTICK_PERIOD_MS); + NeoEsp32RmtHiMethodDriver::Uninstall(_rmtChannel); + esp_err_t instErr = NeoEsp32RmtHiMethodDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); + if (instErr != ESP_OK) { + Serial.printf("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n", instErr); + // Try to fall back to IDF driver + if (rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)) == ESP_OK) { + rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); + _usingRmtHi = false; + } + } else { + _usingRmtHi = true; + } + } } } @@ -188,14 +208,7 @@ bool RmtBus::begin() { if (err != ESP_OK) { return false; } - - // install driver at high priority TODO: need to raise priority just like in RMT high priority driver - err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)); - if (err != ESP_OK) { - return false; - } - - // Register hack for memory blocks normally assigned to RX TODO: is this also needed for S2 / classic ESP32? + // Register hack for memory blocks normally assigned to RX (S3 / C3) #if defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) #if defined(WLEDPB_ESP32S3) for (int i = 4; i < 8; i++) { @@ -208,6 +221,25 @@ bool RmtBus::begin() { #endif #endif + // Try to use the High Priority RMT driver (Neo rmtHi) + { + esp_err_t hiErr = NeoEsp32RmtHiMethodDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); + if (hiErr == ESP_OK) { + _usingRmtHi = true; + _initialized = true; + s_activeChannelMask |= (1 << _channel); + return true; + } else { + Serial.printf("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); + } + } + + // Fallback to IDF rmt driver + translator + err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)); + if (err != ESP_OK) { + return false; + } + err = rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); if (err != ESP_OK) { rmt_driver_uninstall(_rmtChannel); @@ -223,7 +255,11 @@ void RmtBus::end() { if (!_initialized) return; s_activeChannelMask &= ~(1 << _channel); - rmt_driver_uninstall(_rmtChannel); + if (_usingRmtHi) { + NeoEsp32RmtHiMethodDriver::Uninstall(_rmtChannel); + } else { + rmt_driver_uninstall(_rmtChannel); + } if (_encodeBuffer) { free(_encodeBuffer); @@ -232,6 +268,7 @@ void RmtBus::end() { } _initialized = false; + _usingRmtHi = false; } bool RmtBus::allocateBuffer(uint16_t numPixels) { @@ -266,7 +303,26 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc return false; } - // Wait for previous transmission on THIS channel to complete + // If using Neo rmtHi driver, use its APIs + if (_usingRmtHi) { + if (!allocateBuffer(numPixels)) return false; + + // Encode pixels to byte stream + ColorEncoder encoder(_order); + uint8_t* dst = _encodeBuffer; + uint8_t numCh = encoder.getNumChannels(); + + for (uint16_t i = 0; i < numPixels; i++) { + encoder.encode(pixels[i], cct ? &cct[i] : nullptr, dst); + dst += numCh; + } + + // Write() waits for any in-flight transfer internally before starting the new one + size_t dataLen = numPixels * numCh; + return NeoEsp32RmtHiMethodDriver::Write(_rmtChannel, _encodeBuffer, dataLen) == ESP_OK; + } + + // Wait for previous transmission on THIS channel to complete (IDF driver) rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); if (!allocateBuffer(numPixels)) return false; @@ -286,7 +342,7 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; - // Start transmission + // Start transmission (IDF driver) size_t dataLen = numPixels * numCh; esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); @@ -296,12 +352,17 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc bool RmtBus::canShow() const { if (!_initialized) return true; // Use rmt_wait_tx_done with 0 timeout to check if TX is done (matching NeoPixelBus) + if (_usingRmtHi) return (ESP_OK == NeoEsp32RmtHiMethodDriver::WaitForTxDone(_rmtChannel, 0)); return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); } void RmtBus::waitComplete() { if (_initialized) { - rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + if (_usingRmtHi) { + NeoEsp32RmtHiMethodDriver::WaitForTxDone(_rmtChannel, portMAX_DELAY); + } else { + rmt_wait_tx_done(_rmtChannel, portMAX_DELAY); + } } } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 1b1a551000..db54199adb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -2,14 +2,16 @@ #include "WLEDpixelBus.h" +#include "driver/rmt.h" + +#include "NeoEsp32RmtHIMethod.h" + namespace WLEDpixelBus { //============================================================================== // RMT Bus - Works on all ESP32 variants //============================================================================== -#include "driver/rmt.h" - class RmtBus : public IBus { public: /** @@ -52,6 +54,7 @@ class RmtBus : public IBus { ColorOrder _order; bool _inverted; bool _initialized; + bool _usingRmtHi; rmt_channel_t _rmtChannel; uint32_t _rmtBit0; From dc048d62ed9e57166e7060fb7a683f217e4613c1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 18 Mar 2026 21:29:16 +0100 Subject: [PATCH 055/173] disable high priority ISR driver for C3 by default (causes crashes) --- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 635bf0904d..ca219898d7 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -222,6 +222,9 @@ bool RmtBus::begin() { #endif // Try to use the High Priority RMT driver (Neo rmtHi) + // NOTE: rmtHi can deadlock on some cores (notably ESP32-C3). By default we disable it + // on C3 builds, but it can be enabled explicitly with -DWLEDPB_ENABLE_RMT_HI. +#if !defined(WLEDPB_ESP32C3) || defined(WLEDPB_ENABLE_RMT_HI) { esp_err_t hiErr = NeoEsp32RmtHiMethodDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); if (hiErr == ESP_OK) { @@ -233,6 +236,7 @@ bool RmtBus::begin() { Serial.printf("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); } } +#endif // Fallback to IDF rmt driver + translator err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)); From f7a76049a485012a6a661703e5a839675a97b2bf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 20 Mar 2026 17:00:26 +0100 Subject: [PATCH 056/173] refactor: rename ibus to pixelBus, rename polybus to pixelBusAllocator --- wled00/bus_manager.cpp | 22 ++++++++-------- wled00/bus_manager.h | 2 +- wled00/bus_wrapper.h | 32 ++++++++++++------------ wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 4 +-- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 10 +++++--- 5 files changed, 36 insertions(+), 34 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 67543d12c2..ceb4e4557a 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -157,8 +157,8 @@ BusDigital::BusDigital(const BusConfig &bc) 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 - // create bus via PolyBus wrapper which will return a WLEDpixelBus::IBus - _busPtr = PolyBus::create(bc.type, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType, bc.busSpeedFactor); + // create bus via PixelBusAllocator wrapper which will return a WLEDpixelBus::PixelBus + _busPtr = PixelBusAllocator::create(bc.type, _pins, lenToCreate + _skip, (WLEDpixelBus::ColorOrder)bc.colorOrder, _driverType, bc.busSpeedFactor); _valid = (_busPtr != nullptr) && bc.count > 0; // fix for wled#4759 @@ -1197,7 +1197,7 @@ size_t BusConfig::memUsage() const { 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(type, count + skipAmount, pins, driverType); + mem += sizeof(BusDigital) + PixelBusAllocator::memUsage(type, count + skipAmount, pins, driverType); } else if (Bus::isOnOff(type)) { mem += sizeof(BusOnOff); } else { @@ -1267,7 +1267,7 @@ String BusManager::getLEDTypesJSONString() { } bool BusManager::allocateHardware(uint8_t busType, const uint8_t* pins, uint8_t& driverType) { - return PolyBus::allocateHardware(busType, pins, driverType); + return PixelBusAllocator::allocateHardware(busType, pins, driverType); } // Module-level cache for setPixelColor() fast path - must be reset in removeAll() static Bus* _lastBusCache = nullptr; @@ -1281,7 +1281,7 @@ void BusManager::removeAll() { busses.clear(); #ifndef ESP8266 // Reset channel tracking for fresh allocation - PolyBus::resetChannelTracking(); + PixelBusAllocator::resetChannelTracking(); #endif } @@ -1481,12 +1481,12 @@ void BusManager::applyABL() { ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } #ifndef ESP8266 -// PolyBus channel tracking for dynamic allocation -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; -uint8_t PolyBus::_2PchannelsAssigned = 0; -uint8_t PolyBus::_parallelI2sBusType = 0; +// PixelBusAllocator channel tracking for dynamic allocation +uint8_t PixelBusAllocator::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check +uint8_t PixelBusAllocator::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() +uint8_t PixelBusAllocator::_i2sChannelsAssigned = 0; +uint8_t PixelBusAllocator::_2PchannelsAssigned = 0; +uint8_t PixelBusAllocator::_parallelI2sBusType = 0; #endif // Bus static member definition int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin() diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 058e20648a..e6cb66d9f1 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -286,7 +286,7 @@ class BusDigital : public Bus { uint8_t _milliAmpsPerLed; uint16_t _milliAmpsLimit; uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current - WLEDpixelBus::IBus* _busPtr; + WLEDpixelBus::PixelBus* _busPtr; uint32_t* _pixelDataPtr = nullptr; WLEDpixelBus::CctPixel* _cctDataPtr = nullptr; diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index ce6116b03b..2b9984da60 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -65,40 +65,40 @@ inline ProtocolDef getProtocol(uint8_t wledType) { } } -class PolyBus { +class PixelBusAllocator { private: #ifndef ESP8266 static uint8_t _rmtChannelsAssigned; - static uint8_t _rmtChannel; + static uint8_t _rmtChannel; static uint8_t _i2sChannelsAssigned; - static uint8_t _2PchannelsAssigned; + static uint8_t _2PchannelsAssigned; static uint8_t _parallelI2sBusType; // Track first I2S type to enforce parallel timing #endif public: template - static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz) { - if (busPtr) static_cast(busPtr)->begin(); + static void begin(WLEDpixelBus::PixelBus* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz) { + if (busPtr) busPtr->begin(); } - static void* cleanup(void* busPtr, uint8_t busType) { - if (busPtr) delete static_cast(busPtr); + static WLEDpixelBus::PixelBus* cleanup(WLEDpixelBus::PixelBus* busPtr, uint8_t busType) { + if (busPtr) delete busPtr; return nullptr; } - static void show(void* busPtr, uint8_t busType, bool isI2s = false) { - if (busPtr) static_cast(busPtr)->show(); + static void show(WLEDpixelBus::PixelBus* busPtr, uint8_t busType, bool isI2s = false) { + if (busPtr) busPtr->show(); } - static bool canShow(void* busPtr, uint8_t busType) { - if (busPtr) return static_cast(busPtr)->canShow(); + static bool canShow(WLEDpixelBus::PixelBus* busPtr, uint8_t busType) { + if (busPtr) return busPtr->canShow(); return true; } // static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint32_t cW) { // if (busPtr) static_cast(busPtr)->setPixelColor(pix, c); // } - static void setBrightness(void* busPtr, uint8_t busType, uint8_t b) { - if (busPtr) static_cast(busPtr)->setBrightness(b); + static void setBrightness(WLEDpixelBus::PixelBus* busPtr, uint8_t busType, uint8_t b) { + if (busPtr) busPtr->setBrightness(b); } - static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t cW) { - if (busPtr) return static_cast(busPtr)->getPixelColor(pix); + static uint32_t getPixelColor(WLEDpixelBus::PixelBus* busPtr, uint8_t busType, uint16_t pix, uint32_t cW) { + if (busPtr) return busPtr->getPixelColor(pix); return 0; } @@ -141,7 +141,7 @@ class PolyBus { return true; } -static WLEDpixelBus::IBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder, uint8_t driverType = 0, uint8_t busSpeedFactor = 100) { +static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t len, WLEDpixelBus::ColorOrder colorOrder, uint8_t driverType = 0, uint8_t busSpeedFactor = 100) { if (!Bus::isDigital(busType)) return nullptr; #ifndef ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 5ee26b6d15..ef8a81fcf8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -146,7 +146,7 @@ ColorEncoder::ColorEncoder(ColorOrder order) : _order(order), _numChannels(getCh // Bus Factory Implementation //============================================================================== -IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, ColorOrder order, size_t bufferSize, int8_t channel) { +PixelBus* createBus(BusType type, int8_t pin, const LedTiming& timing, ColorOrder order, size_t bufferSize, int8_t channel) { Serial.printf("[WPB] createBus type=%u pin=%d bufSize=%u ch=%d\n", (unsigned)type, pin, bufferSize, channel); if (type == BusType::Auto) { @@ -154,7 +154,7 @@ IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, ColorOrder or Serial.printf("[WPB] Auto resolved to type=%u\n", (unsigned)type); } - IBus* bus = nullptr; + PixelBus* bus = nullptr; switch (type) { #if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index ced2ec2e2d..90987fdfe7 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -185,10 +185,10 @@ constexpr size_t MIN_DMA_BUFFER_SIZE = 256; constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; //============================================================================== -// Base Bus Interface +// Base Pixel Bus Interface //============================================================================== -class IBus { +class PixelBus { protected: uint32_t* _pixelData = nullptr; CctPixel* _cctData = nullptr; @@ -196,7 +196,7 @@ class IBus { uint8_t _brightness = 255; public: - virtual ~IBus() { + virtual ~PixelBus() { if (_pixelData) free(_pixelData); if (_cctData) free(_cctData); } @@ -272,6 +272,8 @@ class IBus { virtual CctPixel* getCctData() { return _cctData; } }; +using IBus = PixelBus; + //============================================================================== // Forward Declarations //============================================================================== @@ -380,7 +382,7 @@ constexpr uint8_t getRmtMaxChannels() { * @param channel RMT channel to use (-1 for auto-allocate) * @return Bus instance (caller owns, delete when done) */ -IBus* createBus(BusType type, int8_t pin, const LedTiming& timing, +PixelBus* createBus(BusType type, int8_t pin, const LedTiming& timing, ColorOrder order, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, int8_t channel = -1); From de9d69a672d9801fae512e03ffe02f3efab581df Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 20 Mar 2026 20:22:35 +0100 Subject: [PATCH 057/173] fix parallel SPI deadlock with timeout, still need to find the root cause --- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 4 +- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 98 +++++++++++-------- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 12 ++- 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 1feee5a94c..dfe6bfdbdc 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -303,7 +303,7 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { // Desired output: [HIGH][data][data][LOW] for each bit // Buffer is always filled completely (zeros = LOW = reset signal) - memset(dest, 0, destLen); + memset(dest, 0, destLen); // TODO: memset is probably not ISR safe size_t pos = 0; // Pre-calculate max channels to speed up loop @@ -366,7 +366,7 @@ void IRAM_ATTR LcdBusContext::encode3Step(uint8_t* dest, size_t destLen) { // Desired output: [HIGH][data][LOW] for each bit // Buffer is always filled completely (zeros = LOW = reset signal) - memset(dest, 0, destLen); + memset(dest, 0, destLen); // TODO: memset is probably not ISR safe size_t pos = 0; uint8_t maxCh = 0; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 809a0af100..c1935872cd 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -75,7 +75,9 @@ SpiBusContext::SpiBusContext() , _stagedMask(0) , _channelMask(0) { - _dmaBuffer[0] = _dmaBuffer[1] = nullptr; + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaBuffer[i] = nullptr; + } for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { _channels[i] = {nullptr, -1, nullptr, 0, false}; } @@ -88,17 +90,19 @@ SpiBusContext::~SpiBusContext() { bool SpiBusContext::isSpiDone() { if (!_hasStarted) return true; // no transfer ever started //return txdone; - // We are logically done once all real data is encoded TODO: should add a timeout here for more accurate end of frame timing - if (!_sending) { - //delay(10); // wait for finish sending !!! this is a hacky test, to check if this locks stuff up. remove again -> seems to fix the glitching and stalls, need a better solution though - return txdone; - } - // Fail-safe watchdog: if stuck for > 500ms, recover it + // Fail-safe watchdog: if stuck for > 50ms, recover it TODO: find a better way to make sure the transmission finishes, the timeout fixes the deadlock if (millis() - _lastTransmitMs > 50) { forceIdle(); return true; } + + // We are logically done once all real data is encoded TODO: should add a timeout here for more accurate end of frame timing + if (!_sending) { + //delay(10); // wait for finish sending !!! this is a hacky test, to check if this locks stuff up. remove again -> seems to fix the glitching and stalls, need a better solution though + return txdone; // if txdone is enforced without a timout, it also seems to cause a deadlock so it is some kind of race condition + } + return false; } @@ -131,20 +135,24 @@ void SpiBusContext::forceIdle() { void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { uint8_t* dst = _dmaBuffer[bufIdx]; - memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); - + uint32_t* dst32 = reinterpret_cast(dst); + for (size_t i = 0; i < (WLEDPB_SPI_DMA_BUFFER_SIZE / 4); i++) { + dst32[i] = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned + } + //memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); // TODO: memset is not ISR IRAM safe? +/* if (!_sending) { return; - } + }*/ size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; if (srcThisChunk == 0) { - _sending = false; - isSending = false; // TODO: integrate this into driver, i.e. non global - _framePos = 0; + //_sending = false; + //isSending = false; // TODO: integrate this into driver, i.e. non global + //_framePos = 0; return; } @@ -187,22 +195,25 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { if (raw & SPI_TRANS_DONE_INT_ST) { txdone = true; // transfer finished + ctx->_sending = false; + isSending = false; // TODO: integrate this into driver, i.e. non global // digitalWrite(0, LOW);//!!! // Serial.print("b"); } else { // Serial.println(raw, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST - // this may be some SPI error, stop SPI and DMA to prevent lockup + // detach pins from spi and force low to prevent any output when cmd.user is stopped (which outputs a fast pattern) //ctx->_hw->cmd.usr = 0; // Stop SPI -> this causes a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks - //ctx->forceIdle(); // -> seems to cause the glitches + ctx->forceIdle(); // -> seems to cause glitches //txdone = true; // still mark it as done - // error, just mark it as done (might not be the best idea... but an attempt to prevent deadlocks)) - txdone = true; - ctx->_sending = false; - isSending = false; + // error, just mark it as done (might not be the best idea... but an attempt to prevent deadlocks)) -> does not prevent deadlocks but seems to prevents glitches + // TODO: need a better solution to handle this isr error + //txdone = true; + //ctx->_sending = false; + //isSending = false; } @@ -213,7 +224,7 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; gdma_dev_t* dma = &GDMA; - + // Serial.print("*"); // toggle gpio0 // digitalWrite(0, HIGH); @@ -224,16 +235,15 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; uint8_t completedBuf = ctx->_currentBuffer; ctx->encodeSpiChunk(completedBuf); - // if (isSending) { // Give ownership of the descriptor back to DMA so it can keep feeding SPI ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? ctx->_dmaDesc[completedBuf].owner = 1; // give ownership back to DMA: it works without this but just make sure its not passed back to CPU - // } - ctx->_currentBuffer = completedBuf ? 0 : 1; // toggle DMA buffer + + ctx->_currentBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; } -// digitalWrite(0, LOW); -dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel + // digitalWrite(0, LOW); + //dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel } bool SpiBusContext::init(const LedTiming& timing) { @@ -243,7 +253,7 @@ bool SpiBusContext::init(const LedTiming& timing) { digitalWrite(0, LOW); //!!! // Allocate DMA buffers - for (int i = 0; i < 2; i++) { + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!_dmaBuffer[i]) { @@ -255,7 +265,7 @@ bool SpiBusContext::init(const LedTiming& timing) { } // Setup DMA descriptors - circular linked list - for (int i = 0; i < 2; i++) { + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; _dmaDesc[i].owner = 1; @@ -263,8 +273,9 @@ bool SpiBusContext::init(const LedTiming& timing) { _dmaDesc[i].eof = 1; _dmaDesc[i].buf = _dmaBuffer[i]; } - _dmaDesc[0].qe.stqe_next = &_dmaDesc[1]; - _dmaDesc[1].qe.stqe_next = &_dmaDesc[0]; + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + _dmaDesc[i].qe.stqe_next = &_dmaDesc[(i + 1) % WLEDPB_SPI_DMA_DESC_COUNT]; + } // Enable peripheral clocks and force-reset (SPI2 + DMA) // periph_module_enable() uses ref counting and may be a no-op if the @@ -328,7 +339,7 @@ bool SpiBusContext::init(const LedTiming& timing) { dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - esp_err_t err = esp_intr_alloc(ETS_DMA_CH0_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3, gdmaISR, this, &_isrHandle); + esp_err_t err = esp_intr_alloc(WLEDPB_SPI_GDMA_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3, gdmaISR, this, &_isrHandle); if (err != ESP_OK) { Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); deinit(); @@ -377,7 +388,7 @@ void SpiBusContext::deinit() { _isrHandle = nullptr; } - for (int i = 0; i < 2; i++) { + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { if (_dmaBuffer[i]) { heap_caps_free(_dmaBuffer[i]); _dmaBuffer[i] = nullptr; @@ -453,6 +464,10 @@ bool SpiBusContext::startTransmit() { _framePos = 0; _numBytes = newBytes; txdone = false;//!!! + _sending = true; + isSending = true; // TODO: integrate this into driver, i.e. non global + _currentBuffer = 0; + // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte // Plus reset: extra zero bits at the end @@ -463,20 +478,17 @@ bool SpiBusContext::startTransmit() { spi_ll_set_mosi_bitlen(_hw, totalBits); spi_ll_clear_int_stat(_hw); - _sending = true; - isSending = true; // TODO: integrate this into driver, i.e. non global - _currentBuffer = 0; - encodeSpiChunk(0); - encodeSpiChunk(1); - - _dmaDesc[0].owner = 1; // make sure the DMA owns the descriptor - _dmaDesc[1].owner = 1; + // Pre-fill all descriptors once to reduce the likelihood of DMA starvation + for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + encodeSpiChunk(i); + _dmaDesc[i].owner = 1; // make sure DMA owns the descriptor + } digitalWrite(0, HIGH);//!!! spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); - //spi_ll_dma_tx_enable(_hw, true); - //spi_ll_apply_config(_hw); + spi_ll_dma_tx_enable(_hw, true); + //spi_ll_apply_config(_hw); // re-attach pins to SPI signals for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { @@ -494,7 +506,7 @@ bool SpiBusContext::startTransmit() { spi_ll_apply_config(_hw); // apply SPI config AFTER starting DMA to make sure they are in sync // Short hardware handshake sync - delayMicroseconds(20); // Short delay to ensure DMA has time to fill SPI buffer: if DMA did not start, SPI will immediately stall with "outfifo_empty_err", short delay makes it less prone (this may be yet another IDF V4 bug) + //delayMicroseconds(20); // Short delay to ensure DMA has time to fill SPI buffer: if DMA did not start, SPI will immediately stall with "outfifo_empty_err", short delay makes it less prone (this may be yet another IDF V4 bug) // Serial.print("a"); dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; @@ -607,7 +619,7 @@ bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctP // Wait for SPI to finish any remaining transfer uint32_t startWait = millis(); while (!_ctx->isSpiDone()) { - if (millis() - startWait > 500) { + if (millis() - startWait > 100) { _ctx->forceIdle(); return false; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index aade34beb5..cf804a581e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -10,8 +10,10 @@ namespace WLEDpixelBus { #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines -#define WLEDPB_SPI_DMA_BUFFER_SIZE 2048 // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (2k per buffer means about 1ms interrupt intervals with 4 step cadence) -#define WLEDPB_SPI_GDMA_CHANNEL 0 +#define WLEDPB_SPI_DMA_BUFFER_SIZE 1024 // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (2k per buffer means about 1ms interrupt intervals with 4 step cadence) +#define WLEDPB_SPI_DMA_DESC_COUNT 2 // number of DMA buffers, 3 = tripple buffering to avoid SPI starvation under heavy load (TODO: 2 is probably more than enough, currently debugging issues...) +#define WLEDPB_SPI_GDMA_CHANNEL 2 // TODO: how to manage the DMA channels to avoid conflicts with other peripherals? for now we just assume channel 1 is free and used exclusively by this driver +#define WLEDPB_SPI_GDMA_INTR_SOURCE ETS_DMA_CH2_INTR_SOURCE // must match dma channel (otherwise it just loops the two DMA descriptors and will eventually time-out) class ParallelSpiBus; @@ -42,7 +44,7 @@ class SpiBusContext { SpiBusContext(); ~SpiBusContext(); - void encodeSpiChunk(uint8_t bufIdx); + void IRAM_ATTR encodeSpiChunk(uint8_t bufIdx); static void IRAM_ATTR gdmaISR(void* arg); static void IRAM_ATTR spiISR(void* arg); @@ -53,8 +55,8 @@ class SpiBusContext { volatile uint8_t _currentBuffer; // DMA - uint8_t* _dmaBuffer[2]; - lldesc_t _dmaDesc[2]; + uint8_t* _dmaBuffer[WLEDPB_SPI_DMA_DESC_COUNT]; + lldesc_t _dmaDesc[WLEDPB_SPI_DMA_DESC_COUNT]; intr_handle_t _isrHandle; intr_handle_t _spiIsrHandle; From 244e03cbf7c1bf83e00622b7afcb6922fd060def Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 20 Mar 2026 20:24:51 +0100 Subject: [PATCH 058/173] deadlock is still not fixed - only if using a single output --- wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index c1935872cd..2c90d2ef37 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -240,7 +240,7 @@ void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? ctx->_dmaDesc[completedBuf].owner = 1; // give ownership back to DMA: it works without this but just make sure its not passed back to CPU - ctx->_currentBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; + ctx->_currentBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; // TODO: reduce again to 2 buffers once it is clear that two are enough (probably is) } // digitalWrite(0, LOW); //dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel From 70d154dd351f819296e219340dc6d91e42ab1f02 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 21 Mar 2026 08:07:09 +0100 Subject: [PATCH 059/173] minor improvement, parallel spi still deadlocks --- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 2c90d2ef37..1376ccf4dc 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -207,13 +207,13 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { //ctx->_hw->cmd.usr = 0; // Stop SPI -> this causes a white flash, can be used to check how often this happens (quite a lot actually, like every few seconds) //spi_ll_dma_tx_enable(ctx->_hw, false); // detacht spi from dma -> does not help with glitches or deadlocks - ctx->forceIdle(); // -> seems to cause glitches + // ctx->forceIdle(); // -> seems to cause glitches -> there are some none iram safe functions in forceIdle! //txdone = true; // still mark it as done // error, just mark it as done (might not be the best idea... but an attempt to prevent deadlocks)) -> does not prevent deadlocks but seems to prevents glitches // TODO: need a better solution to handle this isr error - //txdone = true; - //ctx->_sending = false; - //isSending = false; + //txdone = true; -> do not set txdone, let the timeout handle it to not cause immediate conflicts + ctx->_sending = false; + isSending = false; } @@ -450,24 +450,23 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ bool SpiBusContext::startTransmit() { if (_sending || _channelCount == 0) return false; - if (_stagedMask != _channelMask) return false; // not all channels ready yet + if (_stagedMask != _channelMask) return true; // not all channels ready yet _stagedMask = 0; + _framePos = 0; + txdone = false;//!!! + _sending = true; + isSending = true; // TODO: integrate this into driver, i.e. non global + _currentBuffer = 0; _lastTransmitMs = millis(); size_t newBytes = 0; for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { - if (_channels[ch].active && _channels[ch].srcLen > newBytes) { - newBytes = _channels[ch].srcLen; - } + if (_channels[ch].active && _channels[ch].srcLen > newBytes) { + newBytes = _channels[ch].srcLen; + } } - _framePos = 0; _numBytes = newBytes; - txdone = false;//!!! - _sending = true; - isSending = true; // TODO: integrate this into driver, i.e. non global - _currentBuffer = 0; - // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte // Plus reset: extra zero bits at the end @@ -515,7 +514,6 @@ bool SpiBusContext::startTransmit() { // TODO: there are still some glitches/stalls due to tx_fifo underruns but it is unclear why, it may even be an IDF V4 bug (I read about some timing issue with buffer handover or a missing nop) // even when not accessing the UI there seems to be some dead-lock halting the system until the WDT kicks... - return true; } @@ -603,7 +601,7 @@ bool ParallelSpiBus::allocateBuffer(uint16_t numPixels) { // TODO: this actually only uses internal buffer, could get rid of the parameters that are now unused. bool ParallelSpiBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { - if (!_initialized || !_ctx || !_pixelData || _numPixels == 0) return false; + if (!_initialized || !_ctx) return false; // Wait for previous transmission /* @@ -621,7 +619,7 @@ uint32_t startWait = millis(); while (!_ctx->isSpiDone()) { if (millis() - startWait > 100) { _ctx->forceIdle(); - return false; + break; } vTaskDelay(1); } From 5a47144def7bc0838b372f273c40e59489392c43 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 22 Mar 2026 17:16:25 +0100 Subject: [PATCH 060/173] update driver memory calc, bit rough around the edges but code is there --- wled00/bus_manager.cpp | 6 +- wled00/bus_wrapper.h | 54 ++++++++++--- wled00/data/settings_leds.htm | 81 +++++++++++-------- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 17 ++++ wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 7 ++ wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 7 ++ .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 7 ++ wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 5 ++ wled00/xml.cpp | 14 +++- 9 files changed, 150 insertions(+), 48 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index ceb4e4557a..6368aff65d 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -1279,10 +1279,8 @@ void BusManager::removeAll() { while (!canAllShow()) yield(); _lastBusCache = nullptr; // Reset cache before destroying buses to avoid dangling pointer UB busses.clear(); - #ifndef ESP8266 // Reset channel tracking for fresh allocation PixelBusAllocator::resetChannelTracking(); - #endif } #ifdef ESP32_DATA_IDLE_HIGH @@ -1480,14 +1478,16 @@ void BusManager::applyABL() { ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } -#ifndef ESP8266 // PixelBusAllocator channel tracking for dynamic allocation +uint16_t PixelBusAllocator::_memTrackedDrivers = 0; // bitmask of driver types that have already accounted for shared memory overhead +#ifndef ESP8266 uint8_t PixelBusAllocator::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check uint8_t PixelBusAllocator::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() uint8_t PixelBusAllocator::_i2sChannelsAssigned = 0; uint8_t PixelBusAllocator::_2PchannelsAssigned = 0; uint8_t PixelBusAllocator::_parallelI2sBusType = 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_wrapper.h b/wled00/bus_wrapper.h index 2b9984da60..3ac5b6cc9d 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #ifndef BusWrapper_h #define BusWrapper_h @@ -67,6 +67,8 @@ inline ProtocolDef getProtocol(uint8_t wledType) { class PixelBusAllocator { private: + private: + static uint16_t _memTrackedDrivers; // track which driver types have already accounted for shared memory overhead #ifndef ESP8266 static uint8_t _rmtChannelsAssigned; static uint8_t _rmtChannel; @@ -103,6 +105,7 @@ class PixelBusAllocator { } static void resetChannelTracking() { + _memTrackedDrivers = 0; // reset memory tracking #ifndef ESP8266 _rmtChannelsAssigned = 0; _rmtChannel = 0; @@ -256,25 +259,42 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l busType = _parallelI2sBusType; // Ensure memory matches the hardware configuration } #endif - ProtocolDef proto = getProtocol(busType); + // base PixelBus allocates a 4-byte _pixelData array per pixel + unsigned mem = count * 4; + // and a 2-byte _cctData array per pixel if CCT is enabled + if (Bus::hasCCT(busType)) mem += count * 2; + if (Bus::is2Pin(busType)) { // Basic buffer overhead for SPI - return WLEDpixelBus::estimateMemory(WLEDpixelBus::BusType::Auto, count, proto.channels); + bool isFirst = !(_memTrackedDrivers & (1 << (uint8_t)WLEDpixelBus::BusType::SPI)); + _memTrackedDrivers |= (1 << (uint8_t)WLEDpixelBus::BusType::SPI); + + #if defined(CONFIG_IDF_TARGET_ESP32C3) && defined(WLEDPB_PARALLEL_SPI_SUPPORT) + return mem + WLEDpixelBus::ParallelSpiBus::estimateMemory(count, proto.channels, isFirst); + #else + return mem + WLEDpixelBus::estimateMemory(WLEDpixelBus::BusType::SPI, count, proto.channels); // fallback to base estimate + #endif } auto btype = WLEDpixelBus::BusType::Auto; #ifdef ESP8266 - // Determine the actual bus type from pin number (must mirror the logic in create()) - // Pin 1 → UART0, Pin 2 → UART1, Pin 3 → DMA (I2S), all others → BitBang + btype = WLEDpixelBus::BusType::BitBang; //uint8_t offset = (pins && pins[0] >= 1) ? (pins[0] - 1) : 255; - //if (pins[0] == 1 || pins[0] == 2) btype = WLEDpixelBus::BusType::UART; // GPIO1=TX0, GPIO2=TX1, TX0 is used for debug if enabled - //else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; -> DMA method uses too much RAM (it is also not working right currently) + //if (pins[0] == 1 || pins[0] == 2) btype = WLEDpixelBus::BusType::UART; + //else if (offset == 2) btype = WLEDpixelBus::BusType::DMA; //else btype = WLEDpixelBus::BusType::BitBang; - btype = WLEDpixelBus::BusType::BitBang; + bool isFirst = !(_memTrackedDrivers & (1 << (uint8_t)btype)); + _memTrackedDrivers |= (1 << (uint8_t)btype); + + switch (btype) { + case WLEDpixelBus::BusType::UART: return mem + WLEDpixelBus::Esp8266UartBus::estimateMemory(count, proto.channels, isFirst); + case WLEDpixelBus::BusType::DMA: return mem + WLEDpixelBus::Esp8266DmaBus::estimateMemory(count, proto.channels, isFirst); + default: return mem + WLEDpixelBus::Esp8266BitBangBus::estimateMemory(count, proto.channels, isFirst); + } #else switch (driverType) { case 0: btype = WLEDpixelBus::BusType::RMT; break; @@ -291,9 +311,23 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l break; default: btype = WLEDpixelBus::BusType::RMT; break; } - #endif - return WLEDpixelBus::estimateMemory(btype, count, proto.channels); + bool isFirst = !(_memTrackedDrivers & (1 << (uint8_t)btype)); + _memTrackedDrivers |= (1 << (uint8_t)btype); + + switch (btype) { + #ifdef WLEDPB_LCD_SUPPORT + case WLEDpixelBus::BusType::LCD: return mem + WLEDpixelBus::LcdBus::estimateMemory(count, proto.channels, isFirst); + #endif + #ifdef WLEDPB_I2S_SUPPORT + case WLEDpixelBus::BusType::I2S: return mem + WLEDpixelBus::I2sBus::estimateMemory(count, proto.channels, isFirst); + #endif + #ifdef WLEDPB_PARALLEL_SPI_SUPPORT + case WLEDpixelBus::BusType::SPI: return mem + WLEDpixelBus::ParallelSpiBus::estimateMemory(count, proto.channels, isFirst); + #endif + default: return mem + WLEDpixelBus::RmtBus::estimateMemory(count, proto.channels, isFirst); + } + #endif } }; #endif diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 5e1028198a..c2169adf51 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -6,7 +6,8 @@ LED Settings From 198460dd348b4a8b8dfb2f2c3aaf208c9a53aa84 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 11:51:40 +0200 Subject: [PATCH 140/173] bugfix and indentation --- wled00/data/settings_leds.htm | 152 +++++++++++++++++----------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 59f7c1b668..6a17883851 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -183,7 +183,7 @@ }; if (bquot > 100) {var msg = "Too many LEDs! Can't handle that!"; alert(msg); e.stopPropagation(); return false;} else { - if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory."; + if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.";} if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it) if (d.Sf.checkValidity()) { d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{s.disabled=false;}); // just in case @@ -1138,90 +1138,90 @@ }); } - // Ensure parallel-output bus speed selectors are synchronized: only the first parallel bus - // (I2S/LCD/SPI or BitBang) is editable; all others mirror its value and are disabled. - // This reflects the hardware constraint: I2S, LCD, SPI, and BitBang all use the same - // shared timing and cannot have independent speed factors. - // For custom bus (type 36) on a parallel driver, timing inputs (CBt0h/CBt0l/CBt1h/CBt1l/CBrst) - // are also shared: only the first custom parallel bus can edit them; others are disabled and mirrored. - function syncParallelBusSpeedSelectors() { - try { - // Re-enable all SF selects and custom bus timing inputs by default - // (restores any that stopped being parallel since last UI() call) - d.Sf.querySelectorAll("#mLC select[name^=SF]").forEach(s => { s.disabled = false; s.style.opacity = 1; }); - const cbTimingFields = ["CBt0h","CBt0l","CBt1h","CBt1l","CBrst"]; - cbTimingFields.forEach(fn => { - d.Sf.querySelectorAll("#mLC input[name^="+fn+"]").forEach(el => { el.disabled = false; el.style.opacity = 1; }); - }); + // Ensure parallel-output bus speed selectors are synchronized: only the first parallel bus + // (I2S/LCD/SPI or BitBang) is editable; all others mirror its value and are disabled. + // This reflects the hardware constraint: I2S, LCD, SPI, and BitBang all use the same + // shared timing and cannot have independent speed factors. + // For custom bus (type 36) on a parallel driver, timing inputs (CBt0h/CBt0l/CBt1h/CBt1l/CBrst) + // are also shared: only the first custom parallel bus can edit them; others are disabled and mirrored. + function syncParallelBusSpeedSelectors() { + try { + // Re-enable all SF selects and custom bus timing inputs by default + // (restores any that stopped being parallel since last UI() call) + d.Sf.querySelectorAll("#mLC select[name^=SF]").forEach(s => { s.disabled = false; s.style.opacity = 1; }); + const cbTimingFields = ["CBt0h","CBt0l","CBt1h","CBt1l","CBrst"]; + cbTimingFields.forEach(fn => { + d.Sf.querySelectorAll("#mLC input[name^="+fn+"]").forEach(el => { el.disabled = false; el.style.opacity = 1; }); + }); - let parIndices = []; // all parallel bus IDs (I2S or BitBang) - let custParIndices = []; // subset: custom bus (type 36) on a parallel driver - d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach(sel => { - let n = sel.name.substring(2,3); - let t = parseInt(sel.value); - let drv = d.Sf["LD"+n]?.value | 0; - if (isDig(t) && !isD2P(t) && (drv === 1 || drv === 2 || is8266BB(n))) { - parIndices.push(n); - if (t === 36) custParIndices.push(n); - } - }); + let parIndices = []; // all parallel bus IDs (I2S or BitBang) + let custParIndices = []; // subset: custom bus (type 36) on a parallel driver + d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach(sel => { + let n = sel.name.substring(2,3); + let t = parseInt(sel.value); + let drv = d.Sf["LD"+n]?.value | 0; + if (isDig(t) && !isD2P(t) && (drv === 1 || drv === 2 || is8266BB(n))) { + parIndices.push(n); + if (t === 36) custParIndices.push(n); + } + }); - // Sync speed factor (SF) for all parallel buses (must be the same, hardware restriction) - if (parIndices.length > 0) { - parIndices.sort((a,b)=> toNum(a) - toNum(b)); - const first = parIndices[0]; - for (const n of parIndices) { - let sf = d.getElementsByName("SF"+n)[0]; - if (!sf) continue; - if (n === parIndices[0]) { // first bus - sf.disabled = false; - sf.style.opacity = 1; - } else { - sf.disabled = true; - sf.style.opacity = 0.6; - let fv = d.getElementsByName("SF"+parIndices[0])[0]; - if (fv && sf.value !== fv.value) sf.value = fv.value; - } + // Sync speed factor (SF) for all parallel buses (must be the same, hardware restriction) + if (parIndices.length > 0) { + parIndices.sort((a,b)=> toNum(a) - toNum(b)); + const first = parIndices[0]; + for (const n of parIndices) { + let sf = d.getElementsByName("SF"+n)[0]; + if (!sf) continue; + if (n === parIndices[0]) { // first bus + sf.disabled = false; + sf.style.opacity = 1; + } else { + sf.disabled = true; + sf.style.opacity = 0.6; + let fv = d.getElementsByName("SF"+parIndices[0])[0]; + if (fv && sf.value !== fv.value) sf.value = fv.value; } - let firstEl = d.getElementsByName("SF"+parIndices[0])[0]; - if (firstEl) firstEl.onchange = function() { - let v = this.value; - for (const n of parIndices) if (n !== parIndices[0]) { - let sf = d.getElementsByName("SF"+n)[0]; if (sf) sf.value = v; - } - }; } - - // Sync custom bus timing inputs for parallel custom buses, first custom bus on a parallel driver owns the timing - if (custParIndices.length > 1) { - custParIndices.sort((a,b)=> toNum(a) - toNum(b)); - const firstCust = custParIndices[0]; - for (const n of custParIndices) { - if (n === firstCust) continue; // first bus stays editable - cbTimingFields.forEach(fn => { - let input = d.getElementsByName(fn+n)[0]; - let srcInput = d.getElementsByName(fn+firstCust)[0]; - if (!input) return; - input.disabled = true; - input.style.opacity = 0.6; - if (srcInput && input.value !== srcInput.value) input.value = srcInput.value; - }); + let firstEl = d.getElementsByName("SF"+parIndices[0])[0]; + if (firstEl) firstEl.onchange = function() { + let v = this.value; + for (const n of parIndices) if (n !== parIndices[0]) { + let sf = d.getElementsByName("SF"+n)[0]; if (sf) sf.value = v; } - // Propagate timing changes from first custom bus to all others + }; + } + + // Sync custom bus timing inputs for parallel custom buses, first custom bus on a parallel driver owns the timing + if (custParIndices.length > 1) { + custParIndices.sort((a,b)=> toNum(a) - toNum(b)); + const firstCust = custParIndices[0]; + for (const n of custParIndices) { + if (n === firstCust) continue; // first bus stays editable cbTimingFields.forEach(fn => { + let input = d.getElementsByName(fn+n)[0]; let srcInput = d.getElementsByName(fn+firstCust)[0]; - if (!srcInput) return; - srcInput.oninput = function() { - let v = this.value; - for (const n of custParIndices) if (n !== firstCust) { - let el = d.getElementsByName(fn+n)[0]; if (el) el.value = v; - } - }; + if (!input) return; + input.disabled = true; + input.style.opacity = 0.6; + if (srcInput && input.value !== srcInput.value) input.value = srcInput.value; }); } - } catch(e) {} - } - + // Propagate timing changes from first custom bus to all others + cbTimingFields.forEach(fn => { + let srcInput = d.getElementsByName(fn+firstCust)[0]; + if (!srcInput) return; + srcInput.oninput = function() { + let v = this.value; + for (const n of custParIndices) if (n !== firstCust) { + let el = d.getElementsByName(fn+n)[0]; if (el) el.value = v; + } + }; + }); + } + } catch(e) {} + } + From 94910a205e1cd71bfa6cc9addf93c1279eeaa91e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 12:01:25 +0200 Subject: [PATCH 141/173] fix alert --- wled00/data/settings_leds.htm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 6a17883851..e5bdbf4dfe 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -181,9 +181,9 @@ } } }; - if (bquot > 100) {var msg = "Too many LEDs! Can't handle that!"; alert(msg); e.stopPropagation(); return false;} + if (bquot > 100) {alert("Too many LEDs! Can't handle that!"); e.stopPropagation(); return false;} else { - if (bquot > 80) {var msg = "Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory.";} + if (bquot > 80) alert("Memory usage is high, reboot recommended!\n\rSet transitions to 0 to save memory."); if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it) if (d.Sf.checkValidity()) { d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{s.disabled=false;}); // just in case From d41c4496ea52e4549bc1108236b2c7f29ade80c0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 14:48:59 +0200 Subject: [PATCH 142/173] cleanup and proper SPI assignment on C3 --- wled00/FX_fcn.cpp | 2 -- wled00/bus_manager.cpp | 2 +- wled00/bus_wrapper.h | 20 +++++++++------ wled00/const.h | 7 ++---- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 6 ++--- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 23 +++++------------ wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 26 ++++++++++---------- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 10 ++++---- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 4 ++- 9 files changed, 46 insertions(+), 54 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 735d270938..e6f05b1a31 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1195,8 +1195,6 @@ void WS2812FX::finalizeInit() { i2sBusCount++; else rmtBusCount++; - #else - rmtBusCount++; #endif } } diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 8101623386..1c7ae677d9 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -1574,10 +1574,10 @@ ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } uint8_t PixelBusAllocator::_rmtChannelsAssigned = 0; // number of RMT channels assigned during allocateHardware() uint8_t PixelBusAllocator::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() uint8_t PixelBusAllocator::_i2sChannelsAssigned = 0; -uint8_t PixelBusAllocator::_2PchannelsAssigned = 0; uint8_t PixelBusAllocator::_parallelI2sBusType = 0; uint8_t PixelBusAllocator::_bitBangChannelsAssigned = 0; uint8_t PixelBusAllocator::_bitBangBusType = 0; +uint8_t PixelBusAllocator::_hardwareSPIused = 0; #else uint8_t PixelBusAllocator::_bitBangBusType = 0; #endif diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 2adc7e3bbf..23e70a8068 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -30,10 +30,10 @@ class PixelBusAllocator { static uint8_t _rmtChannelsAssigned; static uint8_t _rmtChannel; static uint8_t _i2sChannelsAssigned; - static uint8_t _2PchannelsAssigned; static uint8_t _parallelI2sBusType; // Track first I2S type to enforce parallel timing static uint8_t _bitBangChannelsAssigned; static uint8_t _bitBangBusType; // Track first BitBang type to enforce parallel timing + static uint8_t _hardwareSPIused; // number of hardware SPI's used, currently only one SPI output is supported. On C3, parallel SPI output takes priority #else static uint8_t _bitBangBusType; // Track first ESP8266 BitBang type to enforce parallel timing #endif @@ -44,10 +44,10 @@ class PixelBusAllocator { _rmtChannelsAssigned = 0; _rmtChannel = 0; _i2sChannelsAssigned = 0; - _2PchannelsAssigned = 0; _parallelI2sBusType = 0; // TYPE_NONE _bitBangChannelsAssigned = 0; _bitBangBusType = 0; // TYPE_NONE + _hardwareSPIused = 0; WLEDpixelBus::RmtBus::resetAutoChannel(); WLEDpixelBus::BitBangBus::resetChannels(); #else @@ -60,10 +60,9 @@ class PixelBusAllocator { if (!Bus::isDigital(busType)) return false; if (Bus::is2Pin(busType)) { - #ifndef ESP8266 - _2PchannelsAssigned++; - #endif - return true; // SPI uses separate pins + // 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) } #ifndef ESP8266 @@ -82,6 +81,9 @@ class PixelBusAllocator { // If first I2S channel request, lock the type to ensure parallel timings match if (_i2sChannelsAssigned == 0) { _parallelI2sBusType = busType; + #ifdef CONFIG_IDF_TARGET_ESP32C3 + _hardwareSPIused++; // reserve SPI: C3 uses prallel SPI output for "I2S" and takes priority over 2pin buses + #endif } _i2sChannelsAssigned++; } else { @@ -137,7 +139,11 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l #ifdef ESP8266 if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; #else - if (_2PchannelsAssigned == 1) isHSPI = true; + if (_hardwareSPIused == 0) { + Serial.println("spi not claimed yet"); + isHSPI = true; + _hardwareSPIused++; // claim hardware SPI (currently only one is supported), on C3 this can also be claimed by parallel SPI (done so in allocateHardware) + } #endif return new WLEDpixelBus::SpiBus(pins[0], pins[1], timing, colorOrder, numChannels, isHSPI, busType); // TODO: move this into createbus function? } diff --git a/wled00/const.h b/wled00/const.h index 9b1f3ddf5d..77a3c49691 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -77,8 +77,8 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 4 // SPI parallel output supported by WLEDpixelBus - //#define WLED_MAX_ANALOG_CHANNELS 6 + #define WLED_MAX_I2S_CHANNELS 4 // uses I2S hardware + //#define WLED_MAX_SPI_CHANNELS 4 // SPI parallel output supported by WLEDpixelBus #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels @@ -87,7 +87,6 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - //#define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels @@ -96,7 +95,6 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - //#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 #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels @@ -105,7 +103,6 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - //#define WLED_MAX_ANALOG_CHANNELS 16 #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_TIMERS 64 // maximum number of timers diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index e7f17d1311..c72cf61dac 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -16,7 +16,7 @@ by @dedehai, 2026 #include "WLEDpixelBus.h" -#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) #include "WLEDpixelBus_RMT.h" #include "WLEDpixelBus_I2S.h" #include "WLEDpixelBus_LCD.h" @@ -205,14 +205,14 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 PixelBus* bus = nullptr; switch (driver) { -#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) case BusDriver::RMT: bus = new RmtBus(pin, timing, colorOrder, numChannels, channel, ledType); break; #ifdef WLEDPB_I2S_SUPPORT case BusDriver::I2S: -#if defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) bus = new I2sBus(pin, timing, colorOrder, numChannels, 0, bufferSize, ledType); #else bus = new I2sBus(pin, timing, colorOrder, numChannels, 1, bufferSize, ledType); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 3511fd1320..6e0ffc2d57 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -29,17 +29,6 @@ by @dedehai, 2026 #include "esp_idf_version.h" #include "driver/rmt.h" -// Platform detection -#if defined(CONFIG_IDF_TARGET_ESP32) - #define WLEDPB_ESP32 -#elif defined(CONFIG_IDF_TARGET_ESP32S2) - #define WLEDPB_ESP32S2 -#elif defined(CONFIG_IDF_TARGET_ESP32S3) - #define WLEDPB_ESP32S3 -#elif defined(CONFIG_IDF_TARGET_ESP32C3) - #define WLEDPB_ESP32C3 -#endif - // I2S support: targets where the I2S peripheral has the LCD Intel 8080 mode // used for parallel LED output. SOC_I2S_LCD_I80_VARIANT is defined on ESP32 and // ESP32-S2; absent on S3 (which has a dedicated LCD CAM) and C3. @@ -54,8 +43,8 @@ by @dedehai, 2026 #endif // SPI parallel support (C3 - uses SPI quad mode with GDMA) -#if defined(WLEDPB_ESP32C3) - #define WLEDPB_PARALLEL_SPI_SUPPORT +#if defined(CONFIG_IDF_TARGET_ESP32C3) + #define WLEDPB_PARALLEL_SPI_SUPPORT #endif // ESP_HAS_HIGH_GPIO_BANK — defined when the target has GPIO > 31 and therefore @@ -541,11 +530,11 @@ enum class BusDriver : uint8_t { * Get the maximum number of RMT TX channels for the current platform */ constexpr uint8_t getRmtMaxChannels() { -#if defined(WLEDPB_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) return 8; // ESP32 original: 8 RMT channels -#elif defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) +#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) return 4; // ESP32-S2/S3: 4 RMT TX channels -#elif defined(WLEDPB_ESP32C3) +#elif defined(CONFIG_IDF_TARGET_ESP32C3) return 2; // ESP32-C3: 2 RMT TX channels #else return 0; @@ -572,7 +561,7 @@ PixelBus* createBus(BusDriver type, int8_t pin, const LedTiming& timing, #include "WLEDpixelBus_SPI.h" -#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32S3) || defined(WLEDPB_ESP32C3) +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) #include "WLEDpixelBus_RMT.h" #include "WLEDpixelBus_I2S.h" #include "WLEDpixelBus_LCD.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 84f5f51060..00b9eadfbb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -47,7 +47,7 @@ I2sBusContext::I2sBusContext(uint8_t busNum) , _stagedMask(0) , _maxDataLen(0) { -#if defined(WLEDPB_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; #else _i2sDev = &I2S0; @@ -105,7 +105,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { } // Enable I2S peripheral -#if defined(WLEDPB_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) periph_module_enable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); #else periph_module_enable(PERIPH_I2S0_MODULE); @@ -155,7 +155,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _i2sDev->lc_conf.out_eof_mode = 1; // Disable PDM -#if defined(WLEDPB_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) _i2sDev->pdm_conf.tx_pdm_en = 0; _i2sDev->pdm_conf.pcm2pdm_conv_en = 0; #endif @@ -165,7 +165,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _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(WLEDPB_ESP32S2) +#if !defined(CONFIG_IDF_TARGET_ESP32S2) _i2sDev->fifo_conf.tx_fifo_mod = 1; //_i2sDev->conf_chan.tx_chan_mod = 0; // Standard mode #else @@ -201,7 +201,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 uint32_t bitPeriodNs = timing.bitPeriod(); -#if defined(WLEDPB_ESP32) +#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. @@ -273,7 +273,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { // Install ISR int intSource; - #if defined(WLEDPB_ESP32) + #if defined(CONFIG_IDF_TARGET_ESP32) intSource = (_busNum == 0) ? ETS_I2S0_INTR_SOURCE : ETS_I2S1_INTR_SOURCE; #else intSource = ETS_I2S0_INTR_SOURCE; @@ -317,7 +317,7 @@ void I2sBusContext::deinit() { } } -#if defined(WLEDPB_ESP32) +#if defined(CONFIG_IDF_TARGET_ESP32) periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); #else periph_module_disable(PERIPH_I2S0_MODULE); @@ -351,18 +351,18 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, bool inverted) { int sigIdx; #ifdef WLED_PIXELBUS_16PARALLEL // 16-bit mode: mapping starts at DATA_OUT8_IDX for the wide 16-bit window - #if defined(WLEDPB_ESP32) + #if defined(CONFIG_IDF_TARGET_ESP32) sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT8_IDX : I2S1O_DATA_OUT8_IDX; - #elif defined(WLEDPB_ESP32S2) + #elif defined(CONFIG_IDF_TARGET_ESP32S2) sigIdx = I2S0O_DATA_OUT8_IDX; #else sigIdx = I2S0O_DATA_OUT0_IDX; #endif #else // 8-bit mode: mapping starts at DATA_OUT0_IDX (S2: DATA_OUT16_IDX for upper byte) - #if defined(WLEDPB_ESP32) + #if defined(CONFIG_IDF_TARGET_ESP32) sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; - #elif defined(WLEDPB_ESP32S2) + #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; @@ -445,7 +445,7 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { // ── Phase 2: Scatter ───────────────────────────────────────────────────── // 16 x 32-bit stores, fully unrolled. uint32_t* p = (uint32_t*)(dest + pos); -#if defined(WLEDPB_ESP32S2) +#if defined(CONFIG_IDF_TARGET_ESP32S2) // S2 layout: [step0, step1, step2, step3] (no half-word swap) // step0=HIGH, step1=data, step2=data, step3=LOW // 32-bit pair: p[0]=(bN<<16)|alwaysMask, p[1]=(0<<16)|bN @@ -513,7 +513,7 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { // = alwaysMask | (bN<<8) | (bN<<16) | 0 // As uint32: alwaysMask | ((uint32_t)bN<<8) | ((uint32_t)bN<<16) uint32_t* p = (uint32_t*)(dest + pos); -#if defined(WLEDPB_ESP32S2) +#if defined(CONFIG_IDF_TARGET_ESP32S2) // S2: no swap, layout [S0,S1,S2,S3] = [HIGH,data,data,LOW] #define EMIT8(bN, OFF) \ p[OFF] = (uint32_t)(alwaysMask) | ((uint32_t)(bN) << 8) | ((uint32_t)(bN) << 16); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index b48600453d..5dec817d48 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -137,9 +137,9 @@ bool RmtBus::begin() { } uint8_t totalBlocks; -#if defined(WLEDPB_ESP32) || defined(WLEDPB_ESP32S3) +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory -#elif defined(WLEDPB_ESP32S2) || defined(WLEDPB_ESP32C3) // note: C6 RMT hardware is the same as C3 +#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) // note: C6 RMT hardware is the same as C3 totalBlocks = 4; // other supported ESP32 variants have 4 blocks #else totalBlocks = 4; // default to 4 if unknown, should be safe @@ -200,11 +200,11 @@ bool RmtBus::begin() { // Register hack for memory blocks normally assigned to RX (S2 / S3 / C3) TODO: need this for ESP32 as well? #ifndef RMT_USE_SINGLE_MEM_BLOCK - #if defined(WLEDPB_ESP32S3) + #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(WLEDPB_ESP32C3) + #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); } @@ -215,7 +215,7 @@ bool RmtBus::begin() { // NOTE: rmtHi can deadlock on some cores (notably ESP32-C3). By default we disable it // on C3 builds, but it can be enabled explicitly with -DWLEDPB_ENABLE_RMT_HI. _usingRmtHi = false; -#if !defined(WLEDPB_ESP32C3) || defined(WLEDPB_ENABLE_RMT_HI) +#if !defined(CONFIG_IDF_TARGET_ESP32C3) || defined(WLEDPB_ENABLE_RMT_HI) esp_err_t hiErr = RmtHiDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); if (hiErr == ESP_OK) { _usingRmtHi = true; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp index a3a60e3c73..cdfdf08f0e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -41,13 +41,15 @@ bool SpiBus::begin() { // 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); + SPI.begin(_clockPin, 127, _dataPin, -1); // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670 + Serial.println("using hardware SPI"); #else SPI.begin(); #endif // Frequency and mode are applied per-frame via beginTransaction(); do NOT // call the deprecated SPI.setFrequency / SPI.setDataMode here. } else { + Serial.println("using software SPI"); pinMode(_dataPin, OUTPUT); pinMode(_clockPin, OUTPUT); // Pre-compute bitmasks so the hot-path bit-bang loop does register writes, From be9d8bc1b48e488f7a9acb1a9d8807027fb1551f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 14:53:05 +0200 Subject: [PATCH 143/173] revert test and debug --- wled00/const.h | 5 ++--- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/wled00/const.h b/wled00/const.h index 77a3c49691..480a2dc3af 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -77,9 +77,8 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels - #define WLED_MAX_I2S_CHANNELS 4 // uses I2S hardware - //#define WLED_MAX_SPI_CHANNELS 4 // SPI parallel output supported by WLEDpixelBus - #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! + #define WLED_MAX_I2S_CHANNELS 4 // uses SPI hardware in 4x parallel output and not actual I2S + #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels #ifdef WLED_PIXELBUS_16PARALLEL diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp index cdfdf08f0e..546449d3a2 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -42,14 +42,12 @@ bool SpiBus::begin() { // so no pin args are needed. #if defined(ARDUINO_ARCH_ESP32) SPI.begin(_clockPin, 127, _dataPin, -1); // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670 - Serial.println("using hardware SPI"); #else SPI.begin(); #endif // Frequency and mode are applied per-frame via beginTransaction(); do NOT // call the deprecated SPI.setFrequency / SPI.setDataMode here. } else { - Serial.println("using software SPI"); pinMode(_dataPin, OUTPUT); pinMode(_clockPin, OUTPUT); // Pre-compute bitmasks so the hot-path bit-bang loop does register writes, From fa68461ff1116739b471e95940dfff36d044267f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 15:33:41 +0200 Subject: [PATCH 144/173] cleanup some comments --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 8 +-- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 8 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 4 +- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 6 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 31 +++++------ wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h | 6 +-- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 53 +++++++------------ 7 files changed, 48 insertions(+), 68 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index c72cf61dac..8e55e0ee04 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -5,12 +5,12 @@ by @dedehai, 2026 Features: - Runtime LED timing configuration -- Double-buffered DMA with interrupt-driven refilling (4-step cadence) - Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 -- RMT, I2S parallel, and LCD parallel output methods +- RMT, I2S parallel, LCD parallel, SPI parallel, BitBang parallel +- 2-Pin LED support (hardware SPI and BitBang) - RGBW uint32_t pixel buffer format (WLED native) -- Separate CCT (WW/CW) buffer support -- IDF v4.x compatible +- Support for up to 6 color channels, 8bit or 16bit +IDF v4.x compatible -------------------------------------------------------------------------*/ diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 6e0ffc2d57..ddd8fdb551 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -5,12 +5,12 @@ by @dedehai, 2026 Features: - Runtime LED timing configuration -- Double-buffered DMA with interrupt-driven refilling (4-step cadence) - Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 -- RMT, I2S parallel, and LCD parallel output methods +- RMT, I2S parallel, LCD parallel, SPI parallel, BitBang parallel +- 2-Pin LED support (hardware SPI and BitBang) - RGBW uint32_t pixel buffer format (WLED native) -- Separate CCT (WW/CW) buffer support -- IDF v4.x compatible +- Support for up to 6 color channels, 8bit or 16bit +IDF v4.x compatible -------------------------------------------------------------------------*/ diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 5dec817d48..2025076bfb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -241,7 +241,7 @@ bool RmtBus::begin() { // route the pin, use hardware signal inversion via GPIO matrix if _inverted //gpio_matrix_out(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); // note: in IDF V5 this is called esp_rom_gpio_connect_out_signal() esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); // TODO: this is the new command in IDF V5, works in V4 too? - + if (_initialized) esp_rom_gpio_connect_out_signal(_pin, RMT_SIG_OUT0_IDX + (int)_rmtChannel, _inverted, false); // TODO: this is the new command in IDF V5, works in V4 too? _initialized = true; @@ -316,7 +316,7 @@ void RmtBus::setColorOrder(uint8_t co) { void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, size_t src_size, size_t wanted_num, size_t* translated_size, size_t* item_num) { -/* +/* // safety check - should never happen if (src == nullptr || dest == nullptr) { *translated_size = 0; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 441bc08b67..c5740a9564 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -7,9 +7,9 @@ namespace WLEDpixelBus { -//============================================================================== +//======================================= // RMT Bus - Works on all ESP32 variants -//============================================================================== +//======================================= class RmtBus : public PixelBus { public: @@ -100,7 +100,7 @@ class RmtBus : public PixelBus { size_t* translated_size, size_t* item_num); // Jump table of callbacks (defined in .cpp). Use 8 entries to match max RMT channels. - static const sample_to_rmt_t s_callbacks[8]; + static const sample_to_rmt_t s_callbacks[8]; // TODO: could define this above and actually use the correct amount of callbacks }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp index 546449d3a2..40e04d8c91 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -36,22 +36,19 @@ bool SpiBus::begin() { _clockHz = timingToClockHz(_timing); if (_useHardware) { - // On ESP32 SPI.begin(sck, miso, mosi, ss) must be called with the actual - // pins so the IO matrix routes the SPI peripheral to the right GPIOs. - // On ESP8266 the hardware SPI uses fixed pins (MOSI=GPIO13, SCK=GPIO14) - // so no pin args are needed. + // On ESP32 SPI.begin(sck, miso, mosi, ss) must be called with the actual pins so the IO matrix routes the SPI peripheral to the right GPIOs. + // On ESP8266 the hardware SPI uses fixed pins (MOSI=GPIO13, SCK=GPIO14) so no pin args are needed. #if defined(ARDUINO_ARCH_ESP32) SPI.begin(_clockPin, 127, _dataPin, -1); // note: in arduino core, -1 means "default" not "none", passing 127 as the MISO pin is a workaround to prevent SPI.begin() assign the default pin, see #5670 #else SPI.begin(); #endif - // Frequency and mode are applied per-frame via beginTransaction(); do NOT - // call the deprecated SPI.setFrequency / SPI.setDataMode here. + // 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(). + // 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); @@ -90,8 +87,7 @@ void SpiBus::end() { } // Fast inline helpers for bit-bang GPIO register access. -// Using W1TS/W1TC (write-1-to-set/clear) registers avoids read-modify-write -// race conditions and is faster than GPIO_OUT read-modify-write. +// Using W1TS/W1TC (write-1-to-set/clear) registers avoids read-modify-write race conditions and is faster than GPIO_OUT read-modify-write. inline void SpiBus::bbSetData(bool high) const { #if defined(ARDUINO_ARCH_ESP32) #ifdef ESP_HAS_HIGH_GPIO_BANK @@ -106,6 +102,7 @@ inline void SpiBus::bbSetData(bool high) const { } else { REG_WRITE(GPIO_OUT_W1TC_REG, _dataMask); } + // TODO: on the C3 BB output is much faster when using direct CPU access through cpu pin groups (see espressif documentation), could potentially achieve several MHz clockspeed #endif #elif defined(ARDUINO_ARCH_ESP8266) if (high) GPOS = _dataMask; else GPOC = _dataMask; @@ -136,8 +133,7 @@ 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. + // 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); @@ -160,11 +156,9 @@ void SpiBus::sendStartFrame(uint16_t numPixels) { } void SpiBus::sendEndFrame(uint16_t numPixels) { - // APA102: ceil(N/16) zero bytes. TODO: NPB seems to send zero bytes, datasheet states four 0xFF bytes is the end frame - // Each APA102 delays the clock by one half-cycle; N LEDs need N/2 extra - // clock pulses to ensure the last pixel latches. One byte = 8 clocks, - // so ceil(N/16) bytes provide the required ceil(N/2) pulses. - // The APA102 datasheet's "4 zero bytes" claim is only valid for N ≤ 64. + // APA102: ceil(N/16) zero bytes. TODO: NPB seems to send zero bytes, datasheet states four 0xFF bytes is the end frame + // Each APA102 delays the clock by one half-cycle; N LEDs need N/2 extra clock pulses to ensure the last pixel latches. One byte = 8 clocks, + // so ceil(N/16) bytes provide the required ceil(N/2) pulses. The APA102 datasheet's "4 zero bytes" claim is only valid for N ≤ 64. // LPD6803: ceil(N/8) zero bytes (one clock per pixel required). // LPD8806: ceil(N/32) 0xFF bytes (high level = latch for MSB-set pixel data). // P9813: fixed 4 zero bytes. @@ -191,8 +185,7 @@ bool SpiBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctP if (_useHardware) { // beginTransaction applies frequency, bit order, and SPI mode atomically. - // This is mandatory on ESP32 — settings applied outside a transaction are - // ignored by the hardware. + // This is mandatory on ESP32 — settings applied outside a transaction are ignored by the hardware. SPI.beginTransaction(SPISettings(_clockHz, MSBFIRST, SPI_MODE0)); } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h index 3f631639d4..9d7eb5a4d9 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -5,9 +5,9 @@ namespace WLEDpixelBus { -//============================================================================== -// SPI Bus (Hardware and Software SPI for 2-wire LEDs like APA102, WS2801) -//============================================================================== +//========================================================================== +// SPI Bus: Hardware and Software SPI for 2-wire LEDs like APA102, WS2801 +//========================================================================== class SpiBus : public PixelBus { public: diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index b43635549d..27b21dc875 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -7,7 +7,7 @@ namespace WLEDpixelBus { // Note on timings regarding I2S/LCD/SPI buses (4 step cadence): // the drivers will always use a 1/4 - 3/4 duty cycle for '0' and '1' bits (e.g. 250ns high + 750ns low for a 1us period) -// it only derives the total period from the given timings for RMT (0hi+0lo+1hi+1lo)/2) +// it derives the total period from the given timings (0hi+0lo+1hi+1lo)/2) // TODO: a better strategy might be to use 0hi*4 as the period as 0 timing is most critical /** @@ -43,42 +43,29 @@ inline LedTiming scaleTiming(const LedTiming& timing, float factor) { return LedTiming(s(timing.t0h_ns), s(timing.t0l_ns), s(timing.t1h_ns), s(timing.t1l_ns), timing.reset_us); } -// LED timing lookup table in flash (PROGMEM on ESP8266, .rodata on ESP32). -// -// WHY PROGMEM: On ESP8266 the linker only sends specific .rodata.* patterns to flash -// (vtables, string literals, RTTI). All other .rodata — including named namespace-scope -// constexpr variables — lands in DRAM. PROGMEM (__attribute__((section(".irom.text")))) -// is the only escape. This table is read once at bus-creation time; the bus stores its own -// copy. Zero permanent DRAM cost. -// -// On ESP32 PROGMEM is a no-op; const data naturally goes to flash via the SPI cache. -// -// Indexed by getTimingIndex() below. The switch returns uint8_t (not a struct) so the -// compiler generates code-based selection, not a DRAM struct-copy table. -// -// Entries with identical timing values are shared: TM1814≡TM1914, UCS8903≡UCS8904, -// APA102≡LPD8806≡P9813≡LPD6803. +// LED timing lookup table in flash (PROGMEM on ESP8266, .rodata on ESP32) +// Indexed by getTimingIndex() below. + static const PROGMEM LedTiming s_ledTimings[] = { -// t0h t0l t1h t1l reset_us -/* 0 WS2812 */ { 300, 900, 700, 500, 100 }, // WS2812B (and 1CH_X3, 2CH_X3, WWA) -/* 1 400KHz */ { 800, 1700, 1600, 900, 300 }, // Generic 400Kbps -/* 2 TM1829 */ { 300, 900, 800, 400, 200 }, // TM1829 -/* 3 UCS8x03*/ { 400, 850, 800, 450, 500 }, // UCS8903 / UCS8904 (16-bit) -/* 4 APA106 */ { 350, 1350, 1350, 350, 50 }, // APA106 / PL9823 -/* 5 TM1x14 */ { 360, 890, 720, 530, 200 }, // TM1914 / TM1814 (same timing) -/* 6 SK6812 */ { 300, 900, 800, 450, 200 }, // SK6812 / SK6812 RGBW -/* 7 TM1815 */ { 740, 1780, 1440, 1060, 200 }, // TM1815 -/* 8 FW1906 */ { 400, 850, 800, 450, 300 }, // FW1906 GRBCW -/* 9 WS2805 */ { 300, 790, 790, 300, 300 }, // WS2805 RGBCW -/*10 SM16825*/ { 300, 900, 900, 300, 80 }, // SM16825 (16-bit) -/*11 SPI */ { 250, 250, 250, 250, 0 }, // APA102 / LPD8806 / P9813 / LPD6803 -/*12 WS2801 */ { 500, 500, 500, 500, 1000 }, // WS2801 +// t0h t0l t1h t1l reset_us + { 300, 900, 700, 500, 100 }, // WS2812B (and 1CH_X3, 2CH_X3, WWA) + { 800, 1700, 1600, 900, 300 }, // Generic 400Kbps + { 300, 900, 800, 400, 200 }, // TM1829 + { 400, 850, 800, 450, 500 }, // UCS8903 / UCS8904 (16-bit) + { 350, 1350, 1350, 350, 50 }, // APA106 / PL9823 + { 360, 890, 720, 530, 200 }, // TM1914 / TM1814 (same timing) + { 300, 900, 800, 450, 200 }, // SK6812 / SK6812 RGBW + { 740, 1780, 1440, 1060, 200 }, // TM1815 + { 400, 850, 800, 450, 300 }, // FW1906 GRBCW + { 300, 790, 790, 300, 300 }, // WS2805 RGBCW + { 300, 900, 900, 300, 80 }, // SM16825 (16-bit) + { 250, 250, 250, 250, 0 }, // APA102 / LPD8806 / P9813 / LPD6803 -> SPI LEDs, timing is not really used + { 500, 500, 500, 500, 1000 }, // WS2801 // TYPE_CUSTOM_BUS (36) timing is passed directly as a LedTiming struct; no fixed entries here. +// TODO: could move these timing into the UI and always pass timings down to the firmware }; -// Maps WLED TYPE_ to an index into s_ledTimings. -// Returns uint8_t — the compiler generates code-based selection, not a 12-byte-per-entry -// DRAM struct table. +// Maps WLED TYPE_ to an index into s_ledTimings static inline uint8_t getTimingIndex(uint8_t wledType) { switch (wledType) { case TYPE_WS2812_RGB: From a264a5dc7ad6ef524374453975e5e27c6d642d2e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 21:36:03 +0200 Subject: [PATCH 145/173] cleanup, add headers --- wled00/bus_wrapper.h | 1 - wled00/const.h | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 13 +++- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 9 ++- .../src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp | 11 ++- .../src/WLEDpixelBus/WLEDpixelBus_BitBang.h | 32 +++------ .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 12 ++++ .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 11 +++ .../src/WLEDpixelBus/WLEDpixelBus_Features.h | 10 +++ wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 67 ++++++++++++------- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 19 ++++++ wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 38 +++++++---- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 24 ++++++- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 26 +++++-- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 17 ++++- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 33 ++++++--- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 16 ++++- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp | 25 +++++-- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h | 10 +++ .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 6 ++ 20 files changed, 289 insertions(+), 93 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 23e70a8068..85ce0e8eb1 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -140,7 +140,6 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; #else if (_hardwareSPIused == 0) { - Serial.println("spi not claimed yet"); 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) } diff --git a/wled00/const.h b/wled00/const.h index 480a2dc3af..09c9de014c 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -66,7 +66,7 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #define WLED_MAX_DIGITAL_CHANNELS 8 #define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S #define WLED_MAX_I2S_CHANNELS 0 - #define WLED_MAX_BB_CHANNELS 8 // ESP8266 parallel bit-bang channels (all share same LED type/timing) + #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! diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 8e55e0ee04..e159ee2c9c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -1,16 +1,23 @@ /*------------------------------------------------------------------------- WLEDpixelBus - Lightweight LED driver library for WLED -by @dedehai, 2026 +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 +- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 and ESP8266 - RMT, I2S parallel, LCD parallel, SPI parallel, BitBang parallel - 2-Pin LED support (hardware SPI and BitBang) - RGBW uint32_t pixel buffer format (WLED native) - Support for up to 6 color channels, 8bit or 16bit -IDF v4.x compatible +- Supports output inversion (ESP32 only) and individual color channel inversion +- Supports hardware LED brightness for improved color resolution if available + +Currently based on IDF v4.x API functions and low-level HAL -------------------------------------------------------------------------*/ diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index ddd8fdb551..d36573f17e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -1,16 +1,19 @@ /*------------------------------------------------------------------------- WLEDpixelBus - Lightweight LED driver library for WLED -by @dedehai, 2026 +written by Damian Schneider @dedehai 2026 Features: - Runtime LED timing configuration -- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 +- Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 and ESP8266 - RMT, I2S parallel, LCD parallel, SPI parallel, BitBang parallel - 2-Pin LED support (hardware SPI and BitBang) - RGBW uint32_t pixel buffer format (WLED native) - Support for up to 6 color channels, 8bit or 16bit -IDF v4.x compatible +- Supports output inversion (ESP32 only) and individual color channel inversion +- Supports hardware LED brightness for improved color resolution if available + +Currently based on IDF v4.x API functions and low-level HAL -------------------------------------------------------------------------*/ diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp index c2ae72573f..5f847beb4e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -1,6 +1,13 @@ /*------------------------------------------------------------------------- -WLEDpixelBus_BitBang — Parallel bit-bang LED output driver for ESP32. -See WLEDpixelBus_BitBang.h for architecture notes. +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +works on all ESP32 variants + +Interrupts are fully disabled during output to avoid any glitches +Each bus can have individual configuration of color channels but all must share the same timing + -------------------------------------------------------------------------*/ #include "WLEDpixelBus_BitBang.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h index 06cb2f51ee..e013214f4c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -1,27 +1,17 @@ /*------------------------------------------------------------------------- -WLEDpixelBus_BitBang — Parallel bit-bang LED output driver for ESP32. - -All configured BitBang channels share a single static context. Each bus -registers its GPIO pin at begin(). When the last registered channel calls -show(), all channels are clocked out in parallel using register-level GPIO -writes and a uint32_t bitmask. - -Constraints / design notes: - • ESP32 only (not ESP8266 — that already has its own BitBangBus). - • GPIO pin ranges per variant: - Classic ESP32: 0–39 (low bank 0–31 via GPIO_OUT_W1TS/TC_REG; - high bank 32–39 via GPIO_OUT1_W1TS/TC_REG) - ESP32-S2: 0–46 (same dual-bank split at pin 32) - ESP32-S3: 0–48 (same dual-bank split at pin 32) - All others (C3/C6/H2): 0–31 (low bank only) - • All BitBang channels MUST use the same LED type / timing (enforced by - PixelBusAllocator when driverType == 2). - • The ISR lock is released and re-acquired after every output bit so that - the FreeRTOS scheduler and time-critical ISRs can still run. If the gap - exceeds the LED latch (reset) threshold the output is aborted. - • driverType value: 2 (0=RMT, 1=I2S/LCD/SPI, 2=BitBang) +WLEDpixelBus — Parallel bit-bang LED output driver + +written by Damian Schneider @dedehai 2026 + +works on all ESP32 variants + +Interrupts are fully disabled during output to avoid any glitches +Each bus can have individual configuration of color channels but all must share the same timing + -------------------------------------------------------------------------*/ + + #pragma once #include "WLEDpixelBus.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index 2d050efd9e..f48b77fa11 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -1,3 +1,15 @@ +/*------------------------------------------------------------------------- + +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" #ifdef WLEDPB_ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index 9c3371500d..6ce63bfd24 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -1,3 +1,14 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - ESP8266 driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +Supports UART and I2S DMA output as well as parallel bit-banging + +-------------------------------------------------------------------------*/ #pragma once #include "WLEDpixelBus.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h index b8940ba639..9951a1ecb8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h @@ -1,3 +1,13 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - special features + +written by Damian Schneider @dedehai 2026 + +prefix data (TM1914), suffix data (SM16825) and brigthness to LED hardware current mapping (TM1814, TM1815 and APA102) + +-------------------------------------------------------------------------*/ + #pragma once #include "../../const.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 00b9eadfbb..31c507b472 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -1,3 +1,23 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel I2S 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 and ESP32 S2 +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.h" #ifdef WLEDPB_I2S_SUPPORT #include "WLEDpixelBus_I2S.h" @@ -169,8 +189,8 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _i2sDev->fifo_conf.tx_fifo_mod = 1; //_i2sDev->conf_chan.tx_chan_mod = 0; // Standard mode #else - _i2sDev->fifo_conf.tx_fifo_mod = 3; - //_i2sDev->conf_chan.tx_chan_mod = 1; + _i2sDev->fifo_conf.tx_fifo_mod = 3; + //_i2sDev->conf_chan.tx_chan_mod = 1; #endif _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold @@ -195,17 +215,16 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _i2sDev->timing.val = 0; // Calculate clock divider for 4-step cadence - // bck_div_num must be >= 2 on ESP32 hardware (NeoPixelBus uses 4) + // bck_div_num must be >= 2 on ESP32 hardware // step_time = clkm_div * bck_div / base_clock_MHz * 1000 ns // clkm_div = step_time_ns * base_clock_MHz / (bck_div * 1000) - const uint8_t bckDiv = 4; // must be >= 2, NeoPixelBus uses 4 + const uint8_t bckDiv = 4; // must be >= 2 uint32_t bitPeriodNs = timing.bitPeriod(); #if defined(CONFIG_IDF_TARGET_ESP32) #ifndef WLED_PIXELBUS_16PARALLEL // 8-bit mode: lcd_tx_wrx2_en=1 halves the effective output rate (WR pulses at BCK/2). // Use 2x clock constant so the divider is doubled, yielding the correct BCK after the factor-of-2. - // (NeoPixelBus uses the same 160MHz constant for parallel 8-bit on ESP32 classic.) const double baseClockMhz = 160.0; #else const double baseClockMhz = 80.0; // 16-bit mode: APB clock, lcd_tx_wrx2_en=0 has no rate halving @@ -214,7 +233,6 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { const double baseClockMhz = 80.0; // S2: 80MHz I2S base clock (wrx2 on S2 does not halve the rate) #endif - // NeoPixelBus formula: clkmdiv = nsBitSendTime / bytesPerSample / dmaBitPerDataBit / bck / 1000 * baseClkMhz // For parallel 8-bit, bytesPerSample=1, dmaBitPerDataBit=4 double clkmdiv = (double)bitPeriodNs / 1.0 / 4.0 / (double)bckDiv / 1000.0 * baseClockMhz; if (clkmdiv < 2.0) clkmdiv = 2.0; @@ -254,7 +272,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _i2sDev->clkm_conf.clkm_div_b = divB; _i2sDev->clkm_conf.clkm_div_num = clkmInteger; - // Sample rate - bck must be >= 2 (NeoPixelBus uses 4) + // Sample rate - bck must be >= 2 _i2sDev->sample_rate_conf.val = 0; _i2sDev->sample_rate_conf.tx_bck_div_num = bckDiv; #ifdef WLED_PIXELBUS_16PARALLEL @@ -264,12 +282,18 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { #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; + _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; @@ -281,13 +305,13 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { esp_err_t err = esp_intr_alloc(intSource, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3, dmaISR, this, &_isrHandle); if (err != ESP_OK) { - Serial.printf("I2S ISR alloc failed: %d", err); + DEBUG_PRINTF("I2S ISR alloc failed: %d", err); deinit(); return false; } _initialized = true; - Serial.printf("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); + DEBUG_PRINTF("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); return true; } @@ -418,10 +442,9 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { } for (size_t pos = 0; pos + 64 <= destLen; pos += 64) { - // ── Phase 1: Gather ─────────────────────────────────────────────────────── // alwaysMask: channels with active data (HIGH step); bN: channels with bit N set uint16_t alwaysMask = 0; - uint16_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; // named regs: compiler keeps in regs + uint16_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; // named regs: compiler should keep in regs uint16_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; for (int ch = 0; ch < maxCh; ch++) { @@ -430,6 +453,7 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { const uint16_t m = (uint16_t)(1u << ch); alwaysMask |= m; const uint8_t b = _channels[ch].srcData[_channels[ch].srcPos++]; + // extract bits, unrolled for speed b0 |= m & (uint16_t)(0u - ((b >> 7) & 1u)); b1 |= m & (uint16_t)(0u - ((b >> 6) & 1u)); b2 |= m & (uint16_t)(0u - ((b >> 5) & 1u)); @@ -442,12 +466,11 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { if (!alwaysMask) break; // no active channels produced data - // ── Phase 2: Scatter ───────────────────────────────────────────────────── // 16 x 32-bit stores, fully unrolled. uint32_t* p = (uint32_t*)(dest + pos); #if defined(CONFIG_IDF_TARGET_ESP32S2) // S2 layout: [step0, step1, step2, step3] (no half-word swap) - // step0=HIGH, step1=data, step2=data, step3=LOW + // step0=HIGH, step1=data, step2=data, step3=LOW (or 0b1000, 0b1110) // 32-bit pair: p[0]=(bN<<16)|alwaysMask, p[1]=(0<<16)|bN #define EMIT(bN, OFF) \ p[OFF] = ((uint32_t)(bN) << 16) | alwaysMask; \ @@ -477,7 +500,6 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { } for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { - // ── Phase 1: Gather ─────────────────────────────────────────────────────── uint8_t alwaysMask = 0; uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; @@ -499,7 +521,6 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { } if (!alwaysMask) break; - // ── Phase 2: Scatter ───────────────────────────────────────────────────── // 8-bit LCD mode with lcd_tx_wrx2_en=1 swaps bytes within 16-bit half-words: // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] (half-word swap within 32-bit word) // Output cadence: [HIGH][data][data][LOW] -> steps [S0,S1,S2,S3] @@ -595,7 +616,7 @@ bool I2sBusContext::startTransmit() { _i2sDev->out_link.start = 1; _i2sDev->conf.tx_start = 1; - // ----- DEBUG BLOCK START ----- + // ----- DEBUG----- /* static uint32_t last_isr = 0; uint32_t diff_isr = s_i2sIsrCount - last_isr; @@ -610,7 +631,7 @@ bool I2sBusContext::startTransmit() { Serial.printf("[I2S-Tx] out_link(0x%08x) lc_conf(0x%08x)\n", _i2sDev->out_link.val, _i2sDev->lc_conf.val); */ - // ----- DEBUG BLOCK END ----- + // ----- DEBUG----- return true; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index 68619f2afc..a04e765292 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -1,3 +1,22 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel I2S 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 and ESP32 S2 +Default is 8 parallel outputs and double DMA buffering but it also supports 16 parallel outputs if needed +For 16 parallel output, triple buffering is required for glitch-free output. +Data is output in 4-step cadence meaning each LED bit is encoded into 4 I2S bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +3k per DMA buffer works well, enough for 32 RGB LEDs in 8x parallel output or roughly 0.9ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + #pragma once #include "WLEDpixelBus.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 26bb2398ba..04e988984b 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -1,3 +1,22 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel 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. + +LCD hardware is available on ESP32 S3 only +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.h" #ifdef WLEDPB_LCD_SUPPORT #include "WLEDpixelBus_LCD.h" @@ -309,7 +328,6 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { } for (size_t pos = 0; pos + 64 <= destLen; pos += 64) { - // ── Phase 1: Gather ─────────────────────────────────────────────────────── uint16_t alwaysMask = 0; uint16_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; uint16_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; @@ -331,7 +349,6 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { } if (!alwaysMask) break; - // ── Phase 2: Scatter ───────────────────────────────────────────────────── // S3 LCD: no byte swapping. // Output cadence [HIGH,data,data,LOW] -> as 32-bit pairs: // p[0] = (bN<<16)|alwaysMask, p[1] = (0<<16)|bN @@ -355,7 +372,6 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { } for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { - // ── Phase 1: Gather ─────────────────────────────────────────────────────── uint8_t alwaysMask = 0; uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; @@ -377,7 +393,6 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { } if (!alwaysMask) break; - // ── Phase 2: Scatter ───────────────────────────────────────────────────── // S3 LCD 8-bit mode (lcd_2byte_en=0): no byte swapping, output [S0,S1,S2,S3]. // Output cadence [HIGH,data,data,LOW]: // As 4 bytes: [alwaysMask, bN, bN, 0] @@ -393,6 +408,8 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { #endif // WLED_PIXELBUS_16PARALLEL +/* +// 3-step cadence implementation, currently unused #ifdef WLED_PIXELBUS_16PARALLEL void IRAM_ATTR LcdBusContext::encode3Step(uint8_t* dest, size_t destLen) { // 3-step cadence encoding for 16-bit parallel output @@ -420,22 +437,14 @@ void IRAM_ATTR LcdBusContext::encode3Step(uint8_t* dest, size_t destLen) { // Unrolled loop for 8 bits // [HIGH][data][LOW] — step 0 always high, step 1 = data bit, step 2 always low (zero from memset) - // bit 7 - p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; } p += 3; - // bit 6 + p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; } p += 3; // bit 7 p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; } p += 3; - // bit 5 p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; } p += 3; - // bit 4 p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; } p += 3; - // bit 3 p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; } p += 3; - // bit 2 p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; } p += 3; - // bit 1 p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; } p += 3; - // bit 0 - p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; } p += 3; + p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; } p += 3; // bit 0 } if (!hasData) break; @@ -450,6 +459,7 @@ void IRAM_ATTR LcdBusContext::encode3Step(uint8_t* dest, size_t destLen) { } } #endif // WLED_PIXELBUS_16PARALLEL (encode3Step) +*/ void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { encode4Step(_dmaBuffer[bufIdx], _bufferSize); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index eadba37659..437c2e7790 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -1,3 +1,22 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel 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. + +LCD hardware is available on ESP32 S3 only +Default is 8 parallel outputs and double DMA buffering but it also supports 16 parallel outputs if needed +For 16 parallel output, triple buffering is required for glitch-free output. +Data is output in 4-step cadence meaning each LED bit is encoded into 4 I2S bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +3k per DMA buffer works well, enough for 32 RGB LEDs in 8x parallel output or roughly 0.9ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + #pragma once #ifdef WLEDPB_LCD_SUPPORT #include "WLEDpixelBus.h" @@ -66,7 +85,7 @@ class LcdBusContext { bool startTransmit(); bool isIdle() const { return _state == DriverState::Idle; } - + void forceIdle() { LCD_CAM.lcd_user.lcd_start = 0; if (_dmaChannel) gdma_stop(_dmaChannel); @@ -82,7 +101,7 @@ class LcdBusContext { void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); void fillBuffer(uint8_t bufIdx); - + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); @@ -119,7 +138,6 @@ class LcdBusContext { static LcdBusContext* _instance; static uint8_t _refCount; - friend class LcdBus; }; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 8946393d8c..cf04bda19d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -1,3 +1,19 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel SPI output driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32 C3 +uses 4 parallel outputs and double DMA buffering +Data is output in 4-step cadence meaning each LED bit is encoded into 4 bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +2k per DMA buffer works well, enough for 42 RGB LEDs or roughly 1.2ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + #include "WLEDpixelBus.h" #ifdef WLEDPB_PARALLEL_SPI_SUPPORT #include "WLEDpixelBus_ParallelSpi.h" @@ -10,9 +26,10 @@ #include "esp_rom_gpio.h" namespace WLEDpixelBus { -//============================================================================== +//============================================= // SPI Parallel Bus Implementation (ESP32-C3) -//============================================================================== +//============================================ + volatile uint32_t spierror = 0; volatile uint32_t encodecalls = 0; volatile uint32_t dmacount = 0; @@ -126,7 +143,7 @@ void SpiBusContext::forceIdle() { _hw->cmd.usr = 0; // stop SPI user transfer (will output a fast clock, so detach pins from spi before this to avoid glitches) _hw->dma_int_clr.val = 0xFFFFFFFF; // clear all SPI interrupt flags } - + spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); @@ -262,8 +279,7 @@ bool SpiBusContext::init(const LedTiming& timing) { // Allocate DMA buffers for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, - MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!_dmaBuffer[i]) { Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); deinit(); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index 9bc6c53f42..59d80893ef 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -1,3 +1,19 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - parallel SPI output driver implementation + +written by Damian Schneider @dedehai 2026 + +supports ESP32 C3 +uses 4 parallel outputs and double DMA buffering +Data is output in 4-step cadence meaning each LED bit is encoded into 4 bits. '0' is 0b1000 and '1' is 0b1110 +Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. +The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size +2k per DMA buffer works well, enough for 42 RGB LEDs or roughly 1.2ms between buffer swaps +Each bus can have individual configuration of color channels but all must share the same timing + +-------------------------------------------------------------------------*/ + #pragma once #include "WLEDpixelBus.h" @@ -8,7 +24,6 @@ namespace WLEDpixelBus { // SPI Parallel Bus - ESP32-C3 (uses SPI2 quad mode + GDMA) //============================================================================== - #define WLEDPB_SPI_MAX_CHANNELS 4 // SPI quad mode = 4 data lines #define WLEDPB_SPI_DMA_BUFFER_SIZE 2048 // must be a multiple of 16 (16 DMA bytes per source byte), clocked out at ~2.6MHz, 4 bits per clock (2k per buffer means about 1ms interrupt intervals with 4 step cadence) #define WLEDPB_SPI_DMA_DESC_COUNT 2 // number of DMA buffers, 3 = tripple buffering to avoid SPI starvation under heavy load (TODO: 2 is probably more than enough, currently debugging issues...) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 2025076bfb..e1959d17bb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -1,3 +1,17 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - RMT output driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +RMT bus works on ESP32, S3, S2 and C3 +Supports auto-distribution of available RMT memory blocks to reduce interrupt frequency - needs to be refined if ever using RMT input +The glitch-free high priority interrupt implementation by @willmmiles is not available on the C3 + +-------------------------------------------------------------------------*/ + #include "WLEDpixelBus.h" #ifdef ARDUINO_ARCH_ESP32 #include "WLEDpixelBus_RMT.h" @@ -12,7 +26,6 @@ namespace WLEDpixelBus { DRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[8] = {}; // Explicit IRAM tranlator callback wrappers for each channel (ensures the function is placed in IRAM which is dropped when using templates) -// TODO: only define the ones actually needed based on available RMT channels, is there an IDF define for this? void IRAM_ATTR RmtBus::translator_ch0(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(0, s, d, ss, w, ts, in); } void IRAM_ATTR RmtBus::translator_ch1(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(1, s, d, ss, w, ts, in); } #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 @@ -78,7 +91,7 @@ void RmtBus::updateRmtTiming() { uint16_t t1h = nsToTicks(_timing.t1h_ns); uint16_t t1l = nsToTicks(_timing.t1l_ns); - Serial.printf("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); + DEBUG_PRINTF("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); rmt_item32_t bit0, bit1; @@ -106,7 +119,7 @@ void RmtBus::updateRmtTiming() { RmtHiDriver::Uninstall(_rmtChannel); esp_err_t instErr = RmtHiDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); if (instErr != ESP_OK) { - Serial.printf("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n", instErr); + DEBUG_PRINTF("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n", instErr); // Try to fall back to IDF driver if (rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)) == ESP_OK) { rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); @@ -130,7 +143,7 @@ bool RmtBus::begin() { // available to the channel to minimize the number of interrupts needed for buffer re-fills (less context switching overhead) // C3 and S3 have less channels than blocks, so the last channel can always use additional blocks // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks - // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 blocks, and CH3 with 5 blocks + // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 block, and CH3 with 5 blocks if (_channel < 0) { if (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) { return false; @@ -166,12 +179,12 @@ bool RmtBus::begin() { } if (_channel >= (int8_t)maxTxChannels) { - Serial.printf("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); + DEBUG_PRINTF("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); return false; } _rmtChannel = (rmt_channel_t)_channel; - Serial.printf("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); + DEBUG_PRINTF("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); updateRmtTiming(); @@ -220,7 +233,7 @@ bool RmtBus::begin() { if (hiErr == ESP_OK) { _usingRmtHi = true; } else { - Serial.printf("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); + DEBUG_PRINTF("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); } #endif @@ -301,7 +314,7 @@ bool RmtBus::canShow() const { } void RmtBus::setTiming(const LedTiming& timing) { - Serial.printf("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); + DEBUG_PRINTF("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); _timing = timing; if (_initialized) { updateRmtTiming(); @@ -313,9 +326,7 @@ void RmtBus::setColorOrder(uint8_t co) { } //note: using O2 optimization has little to no effect on FPS -void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, - size_t src_size, size_t wanted_num, - size_t* translated_size, size_t* item_num) { +void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, size_t src_size, size_t wanted_num, size_t* translated_size, size_t* item_num) { /* // safety check - should never happen if (src == nullptr || dest == nullptr) { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index c5740a9564..ad18528146 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -1,3 +1,17 @@ +/*------------------------------------------------------------------------- + +WLEDpixelBus - RMT output driver implementation + +written by Damian Schneider @dedehai 2026 + +I would like to thank Michael C. Miller (@Makuna), NeoPixelBus helped me figure out the proper hardware initialisation. + +RMT bus works on ESP32, S3, S2 and C3 +Supports auto-distribution of available RMT memory blocks to reduce interrupt frequency - needs to be refined if ever using RMT input +The glitch-free high priority interrupt implementation by @willmmiles is not available on the C3 + +-------------------------------------------------------------------------*/ + #pragma once #include "WLEDpixelBus.h" @@ -8,7 +22,7 @@ namespace WLEDpixelBus { //======================================= -// RMT Bus - Works on all ESP32 variants +// RMT Bus //======================================= class RmtBus : public PixelBus { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp index 40e04d8c91..7e637f0c87 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.cpp @@ -1,3 +1,13 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +supports ESP32, S3, S2 and C3 both hardware SPI and BitBanged output +supports hardware brightness on APA102 for improved color resolution + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + #include "WLEDpixelBus.h" #include "WLEDpixelBus_SPI.h" @@ -5,17 +15,19 @@ #include "soc/gpio_reg.h" #endif +#define SPI_MAX_CLOCK_HZ 20000000UL // maximum SPI clock supported by all target platforms (ESP8266 max is 20 MHz, ESP32 can do 80 MHz but we clamp to 20 MHz for compatibility with ESP8266 and timing accuracy) + namespace WLEDpixelBus { -// Derive SPI clock Hz from timing bitPeriod, clamped to [1 MHz, 20 MHz]. -// APA102 {250,250,250,250,0} -> period=500ns -> 2 MHz -// WS2801 {500,500,500,500,1000} -> period=1000ns -> 1 MHz (clamped min) +// Derive SPI clock Hz from timing bitPeriod, clamped to [1 MHz, SPI_MAX_CLOCK_HZ]. +// bit timing is calculated in busmanager from requested _frequencykHz +// TODO: this could be done more efficiently by storing the value in kHz directly in t0h_ns static uint32_t timingToClockHz(const LedTiming& t) { const uint32_t periodNs = t.bitPeriod(); if (periodNs == 0) return 2000000; uint32_t hz = 1000000000UL / periodNs; if (hz < 1000000) hz = 1000000; // minimum 1 MHz - if (hz > 20000000) hz = 20000000; // maximum 20 MHz // TODO: make this a define and check if ESP8266 can do 40 MHz + if (hz > SPI_MAX_CLOCK_HZ) hz = SPI_MAX_CLOCK_HZ; return hz; } @@ -80,8 +92,13 @@ void SpiBus::end() { if (_useHardware) { SPI.end(); } + #if defined(ARDUINO_ARCH_ESP32) if (_dataPin >= 0) gpio_reset_pin((gpio_num_t)_dataPin); if (_clockPin >= 0) gpio_reset_pin((gpio_num_t)_clockPin); + #else + pinMode(_dataPin, INPUT); + pinMode(_clockPin, INPUT); + #endif if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } _initialized = false; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h index 9d7eb5a4d9..aeb31abef4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -1,3 +1,13 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - SPI 2-pin clocked LEDs driver implementation + +supports ESP32, S3, S2 and C3 both hardware SPI and BitBanged output +supports hardware brightness on APA102 for improved color resolution + +written by Damian Schneider @dedehai 2026 + +-------------------------------------------------------------------------*/ + #pragma once #include "WLEDpixelBus.h" diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 27b21dc875..806ecd6994 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -1,3 +1,9 @@ +/*------------------------------------------------------------------------- +WLEDpixelBus - Lightweight LED driver library for WLED + +written by Damian Schneider @dedehai 2026 +-------------------------------------------------------------------------*/ + #pragma once #include From 6443e78919e4e63f39921c607250ceda6ec48dc8 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 22:36:59 +0200 Subject: [PATCH 146/173] update comment --- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index d36573f17e..5e64184933 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -3,6 +3,10 @@ WLEDpixelBus - Lightweight LED driver library for WLED written by Damian Schneider @dedehai 2026 +I would like to thank Michael C. Miller (@Makuna): +NeoPixelBus served me well as a reference to proper hardware initialisation +as well as figuring out the more exotic digital LED types + Features: - Runtime LED timing configuration - Support for ESP32, ESP32-S2, ESP32-S3, ESP32-C3 and ESP8266 From 9f5058bdeadc58ca00b68cc56e9bc6a9fde8f34e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 22:49:34 +0200 Subject: [PATCH 147/173] fix merge conflics --- wled00/FX_fcn.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 373acbd612..5a6f1030dd 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1291,17 +1291,17 @@ void WS2812FX::finalizeInit() { // update global _pixels[] buffer to match getLengthTotal() note: if allocation fails, WLED will not render anything void WS2812FX::updatePixelBuffer() { - uint32_t requiredMem = getLengthTotal() * sizeof(uint32_t); p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it + _pixels = nullptr; #ifdef ESP8266 // On ESP8266, skip the global framebuffer: blendSegment() encodes directly into bus encode buffers. // This saves N*4 bytes of heap (e.g. 1200B for 300 LEDs, 2000B for 500 LEDs). - _pixels = nullptr; - DEBUG_PRINTF_P(PSTR("ESP8266 direct-bus mode: saved %uB\n"), getLengthTotal() * sizeof(uint32_t)); #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() { From b638e5dc3fe2bdbe9a84caa2648acabadca9cfad Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 25 Jun 2026 23:35:37 +0200 Subject: [PATCH 148/173] comment out debug prints --- wled00/const.h | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/wled00/const.h b/wled00/const.h index d098b212b9..69b29a2aef 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -105,7 +105,7 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_TIMERS 64 // maximum number of timers - #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels (GPIO 0-31 only) + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS + WLED_MAX_BB_CHANNELS) // total number of digital channels (RMT + I2S + BitBang) #endif // WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index 31c507b472..d12da40455 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -305,13 +305,13 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { esp_err_t err = esp_intr_alloc(intSource, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3, dmaISR, this, &_isrHandle); if (err != ESP_OK) { - DEBUG_PRINTF("I2S ISR alloc failed: %d", err); + //DEBUG_PRINTF("I2S ISR alloc failed: %d", err); deinit(); return false; } _initialized = true; - DEBUG_PRINTF("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); + //DEBUG_PRINTF("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); return true; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index e1959d17bb..523b35246d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -91,7 +91,7 @@ void RmtBus::updateRmtTiming() { uint16_t t1h = nsToTicks(_timing.t1h_ns); uint16_t t1l = nsToTicks(_timing.t1l_ns); - DEBUG_PRINTF("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); + //DEBUG_PRINTF("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); rmt_item32_t bit0, bit1; @@ -119,7 +119,7 @@ void RmtBus::updateRmtTiming() { RmtHiDriver::Uninstall(_rmtChannel); esp_err_t instErr = RmtHiDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); if (instErr != ESP_OK) { - DEBUG_PRINTF("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n", instErr); + //DEBUG_PRINTF("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n", instErr); // Try to fall back to IDF driver if (rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)) == ESP_OK) { rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); @@ -179,12 +179,12 @@ bool RmtBus::begin() { } if (_channel >= (int8_t)maxTxChannels) { - DEBUG_PRINTF("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); + //DEBUG_PRINTF("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); return false; } _rmtChannel = (rmt_channel_t)_channel; - DEBUG_PRINTF("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); + //DEBUG_PRINTF("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); updateRmtTiming(); @@ -233,7 +233,7 @@ bool RmtBus::begin() { if (hiErr == ESP_OK) { _usingRmtHi = true; } else { - DEBUG_PRINTF("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); + //DEBUG_PRINTF("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); } #endif @@ -314,7 +314,7 @@ bool RmtBus::canShow() const { } void RmtBus::setTiming(const LedTiming& timing) { - DEBUG_PRINTF("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); + //DEBUG_PRINTF("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); _timing = timing; if (_initialized) { updateRmtTiming(); From 80014ac850fe54b30954646d492583e672fcf5c4 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 04:43:36 +0200 Subject: [PATCH 149/173] fix compile error, remove debug outputs --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 20 ++++++++++--------- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 6 +++--- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 17 ++++++++-------- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 17 ++++++++-------- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 12 +++++------ wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 2 +- 6 files changed, 39 insertions(+), 35 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index d12da40455..ba64eab71d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -37,7 +37,8 @@ I2sBusContext* I2sBusContext::get(uint8_t busNum) { if (_instances[busNum] == nullptr) { _instances[busNum] = new I2sBusContext(busNum); } - _refCount[busNum]++; + if (_instances[busNum] != nullptr) + _refCount[busNum]++; return _instances[busNum]; } @@ -99,7 +100,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); if (!_dmaBuffer[i]) { - Serial.println("I2S DMA buffer alloc failed"); + //Serial.println("I2S DMA buffer alloc failed"); deinit(); return false; } @@ -107,7 +108,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _dmaDesc[i] = (lldesc_t*)heap_caps_aligned_alloc(4, sizeof(lldesc_t), MALLOC_CAP_DMA); if (!_dmaDesc[i]) { - Serial.println("I2S DMA desc alloc failed"); + //Serial.println("I2S DMA desc alloc failed"); deinit(); return false; } @@ -252,10 +253,10 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { _clockDiv = clkmInteger; - Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", bitPeriodNs, clkmInteger, divB, divA, bckDiv); + //Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", bitPeriodNs, clkmInteger, divB, divA, bckDiv); double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; - Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); + //Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); // Set clock (with fractional divider for accurate timing) _i2sDev->clkm_conf.val = 0; @@ -317,7 +318,8 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { void I2sBusContext::deinit() { // wait for finish sending before deinit (just in case) - while (!isIdle()) { vTaskDelay(1); } + int timeout = 100; + while (!isIdle() && timeout--) { vTaskDelay(1); } if (_i2sDev) { _i2sDev->int_ena.val = 0; // Disable interrupts first @@ -568,7 +570,7 @@ bool I2sBusContext::startTransmit() { if (_channelCount == 0) return false; // Only start transmission if ALL active channels have populated data - if (_stagedMask != _channelMask) return false; + if (_stagedMask != _channelMask) return true; _stagedMask = 0; // Reset for next frame _maxDataLen = 0; @@ -731,7 +733,7 @@ bool I2sBus::begin() { _channelIdx = _ctx->registerChannel(_pin, this, _inverted); if (_channelIdx < 0) { - Serial.printf("[I2S] registerChannel failed for pin %d\n", _pin); + DEBUG_PRINTF_P(PSTR("[I2S] registerChannel failed for pin %d\n"), _pin); I2sBusContext::release(_busNum); _ctx = nullptr; return false; @@ -739,7 +741,7 @@ bool I2sBus::begin() { _initialized = true; if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } - Serial.printf("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n", _pin, _busNum, _channelIdx); + DEBUG_PRINTF_P(PSTR("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n"), _pin, _busNum, _channelIdx); return true; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 04e988984b..cf96352eba 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -466,12 +466,12 @@ void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { } bool LcdBusContext::startTransmit() { - if (_stagedMask != _channelMask) return false; // wait for all channels - _stagedMask = 0; - if (_state != DriverState::Idle) return false; if (_channelCount == 0) return false; + if (_stagedMask != _channelMask) return true; // wait for all channels + _stagedMask = 0; + // Reset channel positions _maxDataLen = 0; for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index 437c2e7790..ac75a0bd4a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -37,7 +37,7 @@ namespace WLEDpixelBus { #include "soc/gpio_sig_map.h" #ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE 2048 //2048 -> 2048 works well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) +#define WLEDPB_LCD_DMA_BUFFER_SIZE (3*1024)) // 2048 works as well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) #endif // I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering may be required for 16-parallel output @@ -55,6 +55,14 @@ namespace WLEDpixelBus { #define WLEDPB_LCD_CADENCE_STEPS 4 // TODO: 3-step cadence was mostly abandoned, fully remove it? 4 step cadence is generally better to hit timing targets. #endif +// 16-bit parallel mode supports 16 channels; 8-bit supports 8 channels. +#ifdef WLED_PIXELBUS_16PARALLEL + #define WLEDPB_LCD_MAX_CHANNELS 16 +#else + #define WLEDPB_LCD_MAX_CHANNELS 8 +#endif + + static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); @@ -64,13 +72,6 @@ static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "C static_assert(WLEDPB_LCD_MAX_CHANNELS <= SOC_LCD_I80_BUS_WIDTH, "WLEDPB_LCD_MAX_CHANNELS exceeds hardware LCD bus width (SOC_LCD_I80_BUS_WIDTH)"); -// 16-bit parallel mode supports 16 channels; 8-bit supports 8 channels. -#ifdef WLED_PIXELBUS_16PARALLEL - #define WLEDPB_LCD_MAX_CHANNELS 16 -#else - #define WLEDPB_LCD_MAX_CHANNELS 8 -#endif - class LcdBusContext { public: static LcdBusContext* get(); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index cf04bda19d..9b9770303c 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -124,10 +124,10 @@ bool SpiBusContext::isSpiDone() { // Force SPI idle is a dirty hack to guard against some SPI error/race condition that is really hard to track down, it happens randomly and only if UI refresh plus RMT is involved void SpiBusContext::forceIdle() { - Serial.println("Timeout - Forcing SPI idle..."); + DEBUG_PRINTF_P(PSTR("Timeout - Forcing SPI idle...\n")); // print driver state for debugging - Serial.printf("Channels: %d, FramePos: %d, NumBytes: %d, DMA Count: %d, Encode calls: %d\n", _channelCount, _framePos, _numBytes, dmacount, encodecalls); - if(spierror) Serial.printf("SPI error: %d\n", spierror); + //Serial.printf("Channels: %d, FramePos: %d, NumBytes: %d, DMA Count: %d, Encode calls: %d\n", _channelCount, _framePos, _numBytes, dmacount, encodecalls); + //if(spierror) Serial.printf("SPI error: %d\n", spierror); _txdone = true; _stagedMask = 0; @@ -281,7 +281,7 @@ bool SpiBusContext::init(const LedTiming& timing) { for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!_dmaBuffer[i]) { - Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); + //Serial.printf("[SPI] DMA buffer %d alloc failed\n", i); deinit(); return false; } @@ -365,7 +365,7 @@ bool SpiBusContext::init(const LedTiming& timing) { esp_err_t err = esp_intr_alloc(WLEDPB_SPI_GDMA_INTR_SOURCE, ESP_INTR_FLAG_LEVEL2, gdmaISR, this, &_gdmaIsrHandle); // note: saw no flickering even when using level1 if (err != ESP_OK) { - Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); + //Serial.printf("[SPI] GDMA ISR alloc failed: %d\n", err); deinit(); return false; } @@ -377,7 +377,7 @@ bool SpiBusContext::init(const LedTiming& timing) { // _hw->dma_int_ena.val = 0xFFFFFFFF; // REMOVED: Do not enable all interrupts, they trigger false aborts! err = esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_LEVEL2, spiISR, this, &_spiIsrHandle); if (err != ESP_OK) { - Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); + //Serial.printf("[SPI] SPI ISR alloc failed: %d\n", err); deinit(); return false; } @@ -493,7 +493,8 @@ bool SpiBusContext::startTransmit() { portENTER_CRITICAL(&mux); // Stop SPI and DMA before reconfiguring for new transfer //_hw->cmd.usr = 0; // stop SPI user transfer -> this leads to errors, need to let it "cool down by itself" - while (_hw->cmd.usr); // wait for SPI to actually stop (this step is crucial for stability) + uint32_t timeout = 100; + while (_hw->cmd.usr && timeout--) delay(1); // wait for SPI to actually stop (this step is crucial for stability) spi_ll_clear_int_stat(_hw); // the order of these three commands is important spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); @@ -593,7 +594,7 @@ bool ParallelSpiBus::begin() { _channelIdx = _ctx->registerChannel(_pin, this, _inverted); if (_channelIdx < 0) { - Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); + //Serial.printf("[SPI] registerChannel failed for pin %d\n", _pin); SpiBusContext::release(); _ctx = nullptr; return false; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 523b35246d..0c2c1a67eb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -91,7 +91,7 @@ void RmtBus::updateRmtTiming() { uint16_t t1h = nsToTicks(_timing.t1h_ns); uint16_t t1l = nsToTicks(_timing.t1l_ns); - //DEBUG_PRINTF("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); + //DEBUG_PRINTF_P(PSTR("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n"), _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); rmt_item32_t bit0, bit1; @@ -119,7 +119,7 @@ void RmtBus::updateRmtTiming() { RmtHiDriver::Uninstall(_rmtChannel); esp_err_t instErr = RmtHiDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); if (instErr != ESP_OK) { - //DEBUG_PRINTF("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n", instErr); + //DEBUG_PRINTF_P(PSTR("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n"), instErr); // Try to fall back to IDF driver if (rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)) == ESP_OK) { rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); @@ -179,12 +179,12 @@ bool RmtBus::begin() { } if (_channel >= (int8_t)maxTxChannels) { - //DEBUG_PRINTF("[WPB] RMT channel %d >= max %u, FAIL\n", _channel, maxTxChannels); + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d >= max %u, FAIL\n"), _channel, maxTxChannels); return false; } _rmtChannel = (rmt_channel_t)_channel; - //DEBUG_PRINTF("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n", _channel, blocksToUse, s_allocatedCount, maxTxChannels); + //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d using %u blocks (total allocated: %u/%u)\n"), _channel, blocksToUse, s_allocatedCount, maxTxChannels); updateRmtTiming(); @@ -233,7 +233,7 @@ bool RmtBus::begin() { if (hiErr == ESP_OK) { _usingRmtHi = true; } else { - //DEBUG_PRINTF("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n", hiErr); + //DEBUG_PRINTF_P(PSTR("[WPB] rmtHi Install failed: %d, falling back to IDF driver\n"), hiErr); } #endif @@ -314,7 +314,7 @@ bool RmtBus::canShow() const { } void RmtBus::setTiming(const LedTiming& timing) { - //DEBUG_PRINTF("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n", timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); + //DEBUG_PRINTF_P(PSTR("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n"), timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); _timing = timing; if (_initialized) { updateRmtTiming(); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index ad18528146..efe03c9639 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -16,7 +16,7 @@ The glitch-free high priority interrupt implementation by @willmmiles is not ava #include "WLEDpixelBus.h" #include "driver/rmt.h" -#include "RmtHiDriver.h" +#include "RmtHIDriver.h" #include "esp_rom_gpio.h" // for gpio routing to set inverted signal namespace WLEDpixelBus { From 170ee7a5c48d9227027f02cc5bb594affef1e60d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 04:44:07 +0200 Subject: [PATCH 150/173] fix include --- wled00/src/WLEDpixelBus/RmtHiDriver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp index e9987413f1..3be9ae34ce 100644 --- a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp +++ b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp @@ -9,7 +9,7 @@ by @willmmiles, 2026 #include #include "esp_idf_version.h" -#include "RmtHiDriver.h" +#include "RmtHIDriver.h" #include "soc/soc.h" #include "soc/rmt_reg.h" From ba363f86103a85a11777b19e7fc0df212461d648 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 04:47:27 +0200 Subject: [PATCH 151/173] remove debug code --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 23 -------------------- 1 file changed, 23 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index ba64eab71d..b2e29bd397 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -559,12 +559,6 @@ void I2sBusContext::fillBuffer(uint8_t bufIdx) { // desc->length stays at _bufferSize (set in init, never changes) } -// ----- I2S ISR Tracking ----- -static volatile uint32_t s_i2sIsrCount = 0; -static volatile uint32_t s_i2sIsrSending = 0; -static volatile uint32_t s_i2sIsrReset = 0; -static volatile uint32_t s_i2sIsrIdle = 0; - bool I2sBusContext::startTransmit() { if (_state != DriverState::Idle) return false; if (_channelCount == 0) return false; @@ -618,23 +612,6 @@ bool I2sBusContext::startTransmit() { _i2sDev->out_link.start = 1; _i2sDev->conf.tx_start = 1; - // ----- DEBUG----- - /* - static uint32_t last_isr = 0; - uint32_t diff_isr = s_i2sIsrCount - last_isr; - last_isr = s_i2sIsrCount; - Serial.printf("[I2S-Tx] startTransmit triggering. ISR count delta since last tx: %u\n", diff_isr); - Serial.printf("[I2S-Tx] State vars: isrTotal=%u, send=%u, reset=%u, idle=%u\n", - s_i2sIsrCount, s_i2sIsrSending, s_i2sIsrReset, s_i2sIsrIdle); - Serial.printf("[I2S-Tx] HW Regs: conf(0x%08x) conf1(0x%08x) conf2(0x%08x)\n", - _i2sDev->conf.val, _i2sDev->conf1.val, _i2sDev->conf2.val); - Serial.printf("[I2S-Tx] int_ena(0x%08x) int_raw(0x%08x)\n", - _i2sDev->int_ena.val, _i2sDev->int_raw.val); - Serial.printf("[I2S-Tx] out_link(0x%08x) lc_conf(0x%08x)\n", - _i2sDev->out_link.val, _i2sDev->lc_conf.val); - */ - // ----- DEBUG----- - return true; } From cb47a8de933442c884537f92a55530aa356811ec Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 04:49:53 +0200 Subject: [PATCH 152/173] comment out debug out --- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index b2e29bd397..c1cff9d0a2 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -710,7 +710,7 @@ bool I2sBus::begin() { _channelIdx = _ctx->registerChannel(_pin, this, _inverted); if (_channelIdx < 0) { - DEBUG_PRINTF_P(PSTR("[I2S] registerChannel failed for pin %d\n"), _pin); + //DEBUG_PRINTF_P(PSTR("[I2S] registerChannel failed for pin %d\n"), _pin); I2sBusContext::release(_busNum); _ctx = nullptr; return false; @@ -718,7 +718,7 @@ bool I2sBus::begin() { _initialized = true; if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { end(); return false; } - DEBUG_PRINTF_P(PSTR("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n"), _pin, _busNum, _channelIdx); + //DEBUG_PRINTF_P(PSTR("[I2S] I2sBus::begin() OK: pin=%d, bus=%u, channel=%d\n"), _pin, _busNum, _channelIdx); return true; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 9b9770303c..af340de310 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -124,7 +124,7 @@ bool SpiBusContext::isSpiDone() { // Force SPI idle is a dirty hack to guard against some SPI error/race condition that is really hard to track down, it happens randomly and only if UI refresh plus RMT is involved void SpiBusContext::forceIdle() { - DEBUG_PRINTF_P(PSTR("Timeout - Forcing SPI idle...\n")); + //DEBUG_PRINTF_P(PSTR("Timeout - Forcing SPI idle...\n")); // print driver state for debugging //Serial.printf("Channels: %d, FramePos: %d, NumBytes: %d, DMA Count: %d, Encode calls: %d\n", _channelCount, _framePos, _numBytes, dmacount, encodecalls); //if(spierror) Serial.printf("SPI error: %d\n", spierror); From b61ccdc76956e1239788d1c360dc721be1b700fc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 05:14:16 +0200 Subject: [PATCH 153/173] Fix compile error --- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index ac75a0bd4a..e473c73814 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -37,7 +37,7 @@ namespace WLEDpixelBus { #include "soc/gpio_sig_map.h" #ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE (3*1024)) // 2048 works as well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) +#define WLEDPB_LCD_DMA_BUFFER_SIZE (3*1024) // 2048 works as well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) #endif // I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering may be required for 16-parallel output From 4d4416565f65331c5c65dd6aec5564a74730466b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 05:18:47 +0200 Subject: [PATCH 154/173] remove debug variables --- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index af340de310..7df8274e69 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -30,10 +30,6 @@ namespace WLEDpixelBus { // SPI Parallel Bus Implementation (ESP32-C3) //============================================ -volatile uint32_t spierror = 0; -volatile uint32_t encodecalls = 0; -volatile uint32_t dmacount = 0; - // low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) static inline void spi_ll_apply_config(spi_dev_t *hw) { @@ -124,10 +120,6 @@ bool SpiBusContext::isSpiDone() { // Force SPI idle is a dirty hack to guard against some SPI error/race condition that is really hard to track down, it happens randomly and only if UI refresh plus RMT is involved void SpiBusContext::forceIdle() { - //DEBUG_PRINTF_P(PSTR("Timeout - Forcing SPI idle...\n")); - // print driver state for debugging - //Serial.printf("Channels: %d, FramePos: %d, NumBytes: %d, DMA Count: %d, Encode calls: %d\n", _channelCount, _framePos, _numBytes, dmacount, encodecalls); - //if(spierror) Serial.printf("SPI error: %d\n", spierror); _txdone = true; _stagedMask = 0; @@ -156,7 +148,6 @@ void SpiBusContext::forceIdle() { //note: using O2 optimization has little to no effect on FPS void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { -encodecalls++; uint8_t* dst = _dmaBuffer[bufIdx]; uint32_t* dst32 = reinterpret_cast(dst); for (size_t i = 0; i < (WLEDPB_SPI_DMA_BUFFER_SIZE / 4); i++) { @@ -176,7 +167,6 @@ encodecalls++; //_framePos = 0; return; } - dmacount++; for (uint8_t lane = 0; lane < WLEDPB_SPI_MAX_CHANNELS; lane++) { if (!_channels[lane].active || !_channels[lane].srcData) continue; @@ -214,16 +204,10 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; uint32_t status = ctx->_hw->dma_int_st.val; - spierror = status; if (status & SPI_TRANS_DONE_INT_ST) { ctx->_txdone = true; // transfer finished ctx->_hw->dma_int_clr.val = SPI_TRANS_DONE_INT_ST; // clear flag - spierror=0; - //if(spierror) - // spierror++; - // digitalWrite(0, LOW);//!!! - //Serial.print("b"); } else { // Serial.println(status, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST @@ -238,7 +222,6 @@ void IRAM_ATTR SpiBusContext::spiISR(void* arg) { // now that pins are disconnected, we can stop the user transfer which is causing the fast clock output that leads to glitches ctx->_hw->cmd.usr = 0; // stop SPI user transfer TODO: will this result in a tx done or some other interrupt? -> txdone fires // ctx->_sending = false; - spierror++; // TODO: does not seem to happen ... ever - even if trans done is not firing. ctx->_txdone = true; // set to tx done, need to reset the spi when checking if spi idle } @@ -480,7 +463,6 @@ bool SpiBusContext::startTransmit() { if (_stagedMask != _channelMask) return true; // not all channels ready yet _stagedMask = 0; _txdone = false; - dmacount = 0; // debug hack: make sure rmt is not running during critical init //rmt_channel_t rmtChannel = (rmt_channel_t)0; // TODO: need to track this if using RMT for other purposes, or better, use a separate timer-based approach for WS2812 reset pulse timing @@ -506,9 +488,6 @@ bool SpiBusContext::startTransmit() { newBytes = _channels[ch].srcLen; } } - //if(spierror) Serial.printf("start: SPI error: %d\n", spierror); - //Serial.print("."); - spierror = 0; _numBytes = newBytes; // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte @@ -531,7 +510,6 @@ bool SpiBusContext::startTransmit() { _currentBuffer = 0; // fill all descriptors - encodecalls = 0; for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { encodeSpiChunk(i); _dmaDesc[i].owner = 1; // make sure DMA owns the descriptor From cfb2235baf42f111f3524b1282bf4aef5555d76a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 05:43:57 +0200 Subject: [PATCH 155/173] ifdef cleanup, remove BB for ESP32 and S3 (still needs UI fix) --- wled00/bus_wrapper.h | 2 ++ wled00/const.h | 12 +++++++----- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 8 ++++---- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 7 +++---- wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp | 3 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h | 4 +--- wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 4 ++-- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 6 +++--- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 85ce0e8eb1..b686a6ce13 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -49,7 +49,9 @@ class PixelBusAllocator { _bitBangBusType = 0; // TYPE_NONE _hardwareSPIused = 0; WLEDpixelBus::RmtBus::resetAutoChannel(); + #if (WLED_MAX_BB_CHANNELS > 0) WLEDpixelBus::BitBangBus::resetChannels(); + #endif #else _bitBangBusType = 0; // TYPE_NONE WLEDpixelBus::Esp8266BitBangBus::resetChannels(); diff --git a/wled00/const.h b/wled00/const.h index 69b29a2aef..8265bca984 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -86,15 +86,17 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels + #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels #ifdef WLED_PIXELBUS_16PARALLEL - #define WLED_MAX_I2S_CHANNELS 16 // uses LCD parallel output not I2S and supports up to 16 parallel channels + #define WLED_MAX_I2S_CHANNELS 16 // uses LCD parallel output not I2S and supports up to 16 parallel channels #else #define WLED_MAX_I2S_CHANNELS 8 #endif - #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused + #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! #else #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels #ifdef WLED_PIXELBUS_16PARALLEL @@ -102,10 +104,10 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused + #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_TIMERS 64 // maximum number of timers - #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS + WLED_MAX_BB_CHANNELS) // total number of digital channels (RMT + I2S + BitBang) #endif // WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index e159ee2c9c..0dd962d98b 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -30,7 +30,7 @@ Currently based on IDF v4.x API functions and low-level HAL #include "WLEDpixelBus_ParallelSpi.h" #endif -#if defined(WLEDPB_ESP8266) +#if defined(ESP8266) #include "WLEDpixelBus_ESP8266.h" #endif @@ -238,12 +238,12 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 bus = new ParallelSpiBus(pin, timing, colorOrder, numChannels, ledType); break; #endif - +#if (WLED_MAX_BB_CHANNELS > 0) case BusDriver::BitBang: bus = new BitBangBus(pin, timing, colorOrder, numChannels, ledType); break; - -#elif defined(WLEDPB_ESP8266) +#endif +#elif defined(ESP8266) case BusDriver::UART: bus = new Esp8266UartBus(pin, timing, colorOrder, numChannels, ledType); break; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 5e64184933..aec76e28a8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -71,9 +71,8 @@ Currently based on IDF v4.x API functions and low-level HAL #include "rom/lldesc.h" #endif -#elif defined(ARDUINO_ARCH_ESP8266) - -#define WLEDPB_ESP8266 +#elif defined(ESP8266) +// nothing to do here #else #error "WLEDpixelBus only supports ESP32 and ESP8266 platforms" @@ -574,7 +573,7 @@ PixelBus* createBus(BusDriver type, int8_t pin, const LedTiming& timing, #include "WLEDpixelBus_LCD.h" #include "WLEDpixelBus_ParallelSpi.h" #include "WLEDpixelBus_BitBang.h" -#elif defined(WLEDPB_ESP8266) +#elif defined(ESP8266) #include "WLEDpixelBus_ESP8266.h" #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp index 5f847beb4e..679a54ab40 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -9,11 +9,10 @@ Interrupts are fully disabled during output to avoid any glitches Each bus can have individual configuration of color channels but all must share the same timing -------------------------------------------------------------------------*/ +#if defined(ARDUINO_ARCH_ESP32) && (WLED_MAX_BB_CHANNELS > 0) #include "WLEDpixelBus_BitBang.h" -#if defined(ARDUINO_ARCH_ESP32) - #include "esp_clk.h" // esp_clk_cpu_freq() namespace WLEDpixelBus { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h index e013214f4c..1bb4f52010 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -13,11 +13,9 @@ Each bus can have individual configuration of color channels but all must share #pragma once +#if defined(ARDUINO_ARCH_ESP32) && (WLED_MAX_BB_CHANNELS > 0) #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 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index f48b77fa11..9619af35f7 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -12,7 +12,7 @@ Supports UART and I2S DMA output as well as parallel bit-banging #include "WLEDpixelBus_ESP8266.h" -#ifdef WLEDPB_ESP8266 +#if defined(ESP8266) #include #include @@ -828,4 +828,4 @@ bool Esp8266DmaBus::canShow() const { } // namespace WLEDpixelBus -#endif // WLEDPB_ESP8266 +#endif // ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index 6ce63bfd24..d187068251 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -13,7 +13,7 @@ Supports UART and I2S DMA output as well as parallel bit-banging #include "WLEDpixelBus.h" -#ifdef WLEDPB_ESP8266 +#if defined(ESP8266) namespace WLEDpixelBus { @@ -176,5 +176,5 @@ class Esp8266BitBangBus : public PixelBus { } // namespace WLEDpixelBus -#endif // WLEDPB_ESP8266 +#endif // ESP8266 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index e473c73814..9f2272f500 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -43,11 +43,11 @@ namespace WLEDpixelBus { // I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering may be required for 16-parallel output // TODO: this requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load // TODO2: the buffer count and size need to be checked agains memory usage fomulas, they may be incorrect -#ifndef WLEDPB_I2S_DMA_BUFFER_COUNT +#ifndef WLEDPB_LCD_DMA_BUFFER_COUNT #ifdef WLED_PIXELBUS_16PARALLEL - #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 + #define WLEDPB_LCD_DMA_BUFFER_COUNT 3 #else - #define WLEDPB_I2S_DMA_BUFFER_COUNT 2 + #define WLEDPB_LCD_DMA_BUFFER_COUNT 2 #endif #endif From 0a09f46bb64d6f74ec5a894404e5259f18853e9f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 06:37:48 +0200 Subject: [PATCH 156/173] more ifdef fixes, fix UI for C3 and disabled BB --- wled00/bus_manager.h | 2 +- wled00/const.h | 7 ++++--- wled00/data/settings_leds.htm | 3 +++ wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp | 3 +-- wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h | 5 ++--- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 7e7f0d2266..c6e9585111 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -13,8 +13,8 @@ * Class for addressing various light types */ -#include "src/WLEDpixelBus/WLEDpixelBus.h" #include "const.h" +#include "src/WLEDpixelBus/WLEDpixelBus.h" #include "pin_manager.h" #include #include diff --git a/wled00/const.h b/wled00/const.h index 8265bca984..a0ab963ef3 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -69,7 +69,7 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #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 @@ -78,6 +78,7 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels #define WLED_MAX_I2S_CHANNELS 4 // uses SPI hardware in 4x parallel output and not actual I2S + #define WLED_MAX_BB_CHANNELS 8 // max parallel BitBang channels #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels @@ -95,7 +96,7 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused (saves some flash and RAM) #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! #else #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels @@ -104,7 +105,7 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #else #define WLED_MAX_I2S_CHANNELS 8 #endif - #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused + #define WLED_MAX_BB_CHANNELS 0 // max parallel BitBang channels, 0 means unused (saves some flash and RAM) #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_TIMERS 64 // maximum number of timers diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 62af5c444a..2296c5d200 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -538,6 +538,7 @@ chanuse.style.display = 'inline'; chanuse.style.color = '#ccc'; channelMsg.textContent = `Hardware channels used: RMT ${usage.rmtUsed}/${maxRMT}, I2S ${usage.i2sUsed}/${maxI2S}` + (usage.bbUsed > 0 ? `, BitBang ${usage.bbUsed}/${maxBB}` : ''); + if (isC3) channelMsg.textContent = channelMsg.textContent.replace("I2S", "SPI"); // replace I2S with "SPI" on C3 if (usage.rmtUsed > maxRMT || usage.i2sUsed > maxI2S || usage.bbUsed > maxBB) { chanuse.style.color = 'red'; } @@ -1107,9 +1108,11 @@ let rmtOpt = drvsel.querySelector('option[value="0"]'); let i2sOpt = drvsel.querySelector('option[value="1"]'); let bbOpt = drvsel.querySelector('option[value="2"]'); + if (isC3) i2sOpt.textContent = "SPI"; // rename to SPI on C3 rmtOpt.disabled = false; i2sOpt.disabled = false; if (bbOpt) bbOpt.disabled = false; + if (maxBB == 0) bbOpt.disabled = true; // BB disabled on device, make it non selectable if (curDriver === 0) { if ((usage.i2sUsed >= maxI2S)) i2sOpt.disabled = true; // disable I2S selection on RMT buses if full RMTcount++; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp index 679a54ab40..549b56b04e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -9,10 +9,9 @@ Interrupts are fully disabled during output to avoid any glitches Each bus can have individual configuration of color channels but all must share the same timing -------------------------------------------------------------------------*/ -#if defined(ARDUINO_ARCH_ESP32) && (WLED_MAX_BB_CHANNELS > 0) +#if defined(ARDUINO_ARCH_ESP32) #include "WLEDpixelBus_BitBang.h" - #include "esp_clk.h" // esp_clk_cpu_freq() namespace WLEDpixelBus { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h index 1bb4f52010..8bac9357b1 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -10,10 +10,9 @@ Each bus can have individual configuration of color channels but all must share -------------------------------------------------------------------------*/ - - #pragma once -#if defined(ARDUINO_ARCH_ESP32) && (WLED_MAX_BB_CHANNELS > 0) + +#if defined(ARDUINO_ARCH_ESP32) #include "WLEDpixelBus.h" #include "freertos/FreeRTOS.h" From 71c7912f486be74199872880279b85b0b6bdb2fb Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 06:49:09 +0200 Subject: [PATCH 157/173] fix S3 build --- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 5 +---- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index cf96352eba..033e54e329 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -49,6 +49,7 @@ Key design: // Configuration // ============================================ +#define LCD_LOG(x...) // Serial.printf(x) // define to debug LcdBusContext* LcdBusContext::_instance = nullptr; uint8_t LcdBusContext::_refCount = 0; @@ -551,10 +552,6 @@ IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, return false; // Do not yield OS for this DMA streaming interrupt } -void LcdBusContext::printDebugStats() { - LCD_LOG("state=%u, channels=%u, mask=0x%04X", (unsigned)_state, _channelCount, _channelMask); -} - // ============================================ // LcdBus Implementation // ============================================ diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index 9f2272f500..7d8a5e1bd0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -94,7 +94,6 @@ class LcdBusContext { } void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); - void printDebugStats(); private: LcdBusContext(); From b04947ae9f801c64d638acee72a73a12a788febe Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 08:17:28 +0200 Subject: [PATCH 158/173] revert "optimization" to ABL, remove double gamma application --- wled00/bus_manager.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 2ea76bc8a1..b015653265 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -284,12 +284,16 @@ void BusDigital::applyBriLimit(uint8_t newBri) { if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus newBri = 255; - if (_milliAmpsTotal > _milliAmpsLimit) { + if (_milliAmpsLimit > getLength()) { // each LED uses about 1mA in standby + if (_milliAmpsTotal > _milliAmpsLimit) { // scale brightness down to stay in current limit newBri = ((uint32_t)_milliAmpsLimit * 255) / _milliAmpsTotal + 1; // +1 to avoid 0 brightness - if (newBri == 0) newBri = 1; // safety clamp _milliAmpsTotal = _milliAmpsLimit; } + } else { + newBri = 1; // limit too low, set brightness to 1, this will dim down all colors to minimum since we use video scaling + _milliAmpsTotal = getLength(); // estimate bus current as minimum + } } if (newBri < 255) { @@ -329,9 +333,9 @@ void BusDigital::setStatusPixel(uint32_t c) { } } -// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash // TODO: this function may still need some optimization, making better use of the new bus architecture -// TODO: is there any benefit of putting this in IRAM on ESP32? in IRAM: 34.2fps, no IRAM: 34.3fps -> no benefit. removed IRAM_ATTR for now +// 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 (_reversed) pix = _len - pix -1; @@ -339,7 +343,6 @@ void BusDigital::setPixelColor(unsigned pix, uint32_t c) { uint8_t cctWW = 0, cctCW = 0; uint16_t wwcw = 0; if (c > 0) { - c = gamma32(c); // apply gamma correction (table is unity when realtime disables gamma) if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT if (hasWhite()) { c = autoWhiteCalc(c, cctWW, cctCW); From 358d967954ffcb69c86d8f93987efb7926e2620f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 10:58:26 +0200 Subject: [PATCH 159/173] replace static members with struct in ESP8266 BB bus --- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 145 ++++++++---------- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 31 ++-- 2 files changed, 79 insertions(+), 97 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index 9619af35f7..5fbb687a34 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -177,17 +177,7 @@ bool Esp8266UartBus::canShow() const { // ESP8266 BitBang Bus (parallel output across multiple pins) //============================================================================== -// Static member definitions -int8_t Esp8266BitBangBus::s_pins[WLED_MAX_BB_CHANNELS] = {}; -uint16_t Esp8266BitBangBus::s_numPixels[WLED_MAX_BB_CHANNELS] = {}; -uint8_t* Esp8266BitBangBus::s_pixelData[WLED_MAX_BB_CHANNELS] = {}; -uint8_t Esp8266BitBangBus::s_channelCount = 0; -uint32_t Esp8266BitBangBus::s_allMask = 0; -uint8_t Esp8266BitBangBus::s_stagedCount = 0; -uint32_t Esp8266BitBangBus::s_t0h = 0; -uint32_t Esp8266BitBangBus::s_t1h = 0; -uint32_t Esp8266BitBangBus::s_period = 0; -uint8_t Esp8266BitBangBus::s_pixelBytes = 0; +Esp8266BitBangBus::BBstate* Esp8266BitBangBus::_BBs = nullptr; Esp8266BitBangBus::Esp8266BitBangBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) : _pin(pin), _timing(timing), _initialized(false) { @@ -202,7 +192,11 @@ Esp8266BitBangBus::~Esp8266BitBangBus() { bool Esp8266BitBangBus::begin() { if (_initialized) return true; if (_pin < 0 || _pin > 15) return false; // Only GPIO 0-15 are output-capable on ESP8266 - if (s_channelCount >= WLED_MAX_BB_CHANNELS) return false; + if (!_BBs) { + _BBs = (BBstate*)calloc(1, sizeof(BBstate)); // Allocate shared state struct on first channel + if (!_BBs) return false; + } + if (_BBs->channelCount >= WLED_MAX_BB_CHANNELS) return false; pinMode(_pin, OUTPUT); digitalWrite(_pin, LOW); @@ -218,46 +212,50 @@ bool Esp8266BitBangBus::begin() { uint32_t period = (_timing.bitPeriod() * cpuMHz) / 1000u; period = (period > 1u) ? period - 1u : 0u; - // Register in the shared static table - const uint8_t idx = s_channelCount; - s_pins[idx] = _pin; - s_allMask |= (1u << _pin); - s_channelCount++; + // Register in the shared state table + const uint8_t idx = _BBs->channelCount; + _BBs->pins[idx] = _pin; + _BBs->allMask |= (1u << _pin); + _BBs->channelCount++; if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { - s_channelCount--; - s_allMask &= ~(1u << _pin); + _BBs->channelCount--; + _BBs->allMask &= ~(1u << _pin); return false; } - s_numPixels[idx] = _numPixels; - s_pixelData[idx] = _pixelData; - // Timing is shared — same for all channels (enforced by PixelBusAllocator). - s_t0h = t0h; - s_t1h = t1h; - s_period = period; - s_pixelBytes = _encoder.getPixelBytes(); + _BBs->numPixels[idx] = _numPixels; + _BBs->pixelData[idx] = _pixelData; + _BBs->t0h = t0h; // Timing is shared — same for all channels (enforced by PixelBusAllocator). + _BBs->t1h = t1h; + _BBs->period = period; + _BBs->pixelBytes = _encoder.getPixelBytes(); _initialized = true; return true; } void Esp8266BitBangBus::end() { - if (_initialized) { - uint8_t slot = s_channelCount; // sentinel - for (uint8_t i = 0; i < s_channelCount; i++) { - if (s_pins[i] == _pin) { slot = i; break; } + if (_initialized && _BBs) { + uint8_t slot = _BBs->channelCount; + for (uint8_t i = 0; i < _BBs->channelCount; i++) { + if (_BBs->pins[i] == _pin) { slot = i; break; } } - if (slot < s_channelCount) { - for (uint8_t i = slot; i + 1 < s_channelCount; i++) { - s_pins[i] = s_pins[i + 1]; - s_numPixels[i] = s_numPixels[i + 1]; - s_pixelData[i] = s_pixelData[i + 1]; + if (slot < _BBs->channelCount) { + for (uint8_t i = slot; i + 1 < _BBs->channelCount; i++) { + _BBs->pins[i] = _BBs->pins[i + 1]; + _BBs->numPixels[i] = _BBs->numPixels[i + 1]; + _BBs->pixelData[i] = _BBs->pixelData[i + 1]; } - s_channelCount--; - s_allMask = 0; - for (uint8_t i = 0; i < s_channelCount; i++) s_allMask |= (1u << s_pins[i]); - s_stagedCount = 0; + _BBs->channelCount--; + _BBs->allMask = 0; + for (uint8_t i = 0; i < _BBs->channelCount; i++) + _BBs->allMask |= (1u << _BBs->pins[i]); + _BBs->stagedCount = 0; + } + if (_BBs->channelCount == 0) { + free(_BBs); + _BBs = nullptr; } } pinMode(_pin, INPUT); @@ -273,7 +271,7 @@ void Esp8266BitBangBus::end() { void Esp8266BitBangBus::setTiming(const LedTiming& timing) { _timing = timing; // Recompute cycle counts if already initialized - if (_initialized) { + if (_initialized && _BBs) { const uint32_t cpuMHz = ESP.getCpuFreqMHz(); uint32_t t0h = (_timing.t0h_ns * cpuMHz) / 1000u; uint32_t t1h = (_timing.t1h_ns * cpuMHz) / 1000u; @@ -281,9 +279,9 @@ void Esp8266BitBangBus::setTiming(const LedTiming& timing) { t1h = (t1h > 1u) ? t1h - 1u : 0u; uint32_t period = (_timing.bitPeriod() * cpuMHz) / 1000u; period = (period > 1u) ? period - 1u : 0u; - s_t0h = t0h; - s_t1h = t1h; - s_period = period; + _BBs->t0h = t0h; + _BBs->t1h = t1h; + _BBs->period = period; } } @@ -291,14 +289,14 @@ void Esp8266BitBangBus::setTiming(const LedTiming& timing) { IRAM_ATTR bool Esp8266BitBangBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { if (!_initialized || !_pixelData) return false; - s_stagedCount++; - if (s_stagedCount < s_channelCount) { + _BBs->stagedCount++; + if (_BBs->stagedCount < _BBs->channelCount) { return true; // not all channels ready yet } // Last channel staged — output all channels in parallel - s_stagedCount = 0; - return outputParallel(); + _BBs->stagedCount = 0; // reset for next frame + return outputParallel(); // send data to LEDs (blocking) } void Esp8266BitBangBus::setColorOrder(uint8_t co) { @@ -309,16 +307,7 @@ void Esp8266BitBangBus::setColorOrder(uint8_t co) { // resetChannels() — called by PixelBusAllocator::resetChannelTracking() // --------------------------------------------------------------------------- void Esp8266BitBangBus::resetChannels() { - s_channelCount = 0; - s_allMask = 0; - s_stagedCount = 0; - s_t0h = 0; - s_t1h = 0; - s_period = 0; - s_pixelBytes = 0; - memset(s_pins, 0, sizeof(s_pins)); - memset(s_numPixels, 0, sizeof(s_numPixels)); - memset(s_pixelData, 0, sizeof(s_pixelData)); + if (_BBs) memset(_BBs, 0, sizeof(BBstate)); } // --------------------------------------------------------------------------- @@ -337,19 +326,19 @@ void Esp8266BitBangBus::resetChannels() { // channel; for N channels it is identical (all clocked in parallel, same time). // --------------------------------------------------------------------------- bool IRAM_ATTR Esp8266BitBangBus::outputParallel() { - if (s_channelCount == 0) return true; + if (!_BBs || _BBs->channelCount == 0) return true; - const uint32_t t0h = s_t0h; - const uint32_t t1h = s_t1h; - const uint32_t period = s_period; - const uint32_t setAllMask = s_allMask; - const uint8_t pixelBytes = s_pixelBytes; - const uint8_t nCh = s_channelCount; + const uint32_t t0h = _BBs->t0h; + const uint32_t t1h = _BBs->t1h; + const uint32_t period = _BBs->period; + const uint32_t setAllMask = _BBs->allMask; + const uint8_t pixelBytes = _BBs->pixelBytes; + const uint8_t nCh = _BBs->channelCount; // Find maximum pixel count across all channels uint16_t maxPixels = 0; for (uint8_t ch = 0; ch < nCh; ch++) { - if (s_numPixels[ch] > maxPixels) maxPixels = s_numPixels[ch]; + if (_BBs->numPixels[ch] > maxPixels) maxPixels =_BBs->numPixels[ch]; } if (maxPixels == 0) return true; @@ -359,20 +348,19 @@ bool IRAM_ATTR Esp8266BitBangBus::outputParallel() { uint32_t chanPinMask[nCh]; uint32_t chanTotalBytes[nCh]; for (uint8_t ch = 0; ch < nCh; ch++) { - chanPinMask[ch] = 1u << s_pins[ch]; - chanTotalBytes[ch] = (uint32_t)s_numPixels[ch] * pixelBytes; + chanPinMask[ch] = 1u << _BBs->pins[ch]; + chanTotalBytes[ch] = (uint32_t)_BBs->numPixels[ch] * pixelBytes; } // Inline lambda equivalent: compute bitmask of channels outputting '0' for this bit - // (channels past their data end also output '0'). - // MSB-first within each byte. + // Channels past their data end also output '0'. MSB-first within each byte. // TODO: there may be room to speed this up, it is quite slow and takes like 2us auto computeZeroMask = [&](uint32_t bitIndex) -> uint32_t { const uint32_t byteIndex = bitIndex >> 3; const uint8_t bitMask = 0x80u >> (bitIndex & 7u); uint32_t zm = 0; for (uint8_t ch = 0; ch < nCh; ch++) { - if (byteIndex >= chanTotalBytes[ch] || !(s_pixelData[ch][byteIndex] & bitMask)) { + if (byteIndex >= chanTotalBytes[ch] || !(_BBs->pixelData[ch][byteIndex] & bitMask)) { zm |= chanPinMask[ch]; } } @@ -385,23 +373,16 @@ bool IRAM_ATTR Esp8266BitBangBus::outputParallel() { os_intr_lock(); for (uint32_t bitIndex = 0; bitIndex < totalBits; bitIndex++) { + uint32_t zeroMask = computeZeroMask(bitIndex); // Compute zero mask for this bit + while ((ESP.getCycleCount() - cyclesStart) < period); // Wait for the full bit period since the last HIGH edge - // ── Step 1: Compute zero mask for this bit ──────────────────────────── - uint32_t zeroMask = computeZeroMask(bitIndex); - - // ── Step 2: Wait for the full bit period since the last HIGH edge ────── - while ((ESP.getCycleCount() - cyclesStart) < period); - - // ── Step 3: Set all outputs HIGH simultaneously ─────────────────────── - GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, setAllMask); + GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, setAllMask); //Set all outputs HIGH simultaneously cyclesStart = ESP.getCycleCount(); - // ── Step 4: After T0H — pull '0' outputs LOW ───────────────────────── - while ((ESP.getCycleCount() - cyclesStart) < t0h); + while ((ESP.getCycleCount() - cyclesStart) < t0h); // After T0H — pull '0' outputs LOW GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, zeroMask); - // ── Step 5: After T1H — pull all remaining outputs LOW ─────────────── - while ((ESP.getCycleCount() - cyclesStart) < t1h); + while ((ESP.getCycleCount() - cyclesStart) < t1h); // After T1H — pull all remaining outputs LOW GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, setAllMask); } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index d187068251..cf4af13679 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -153,22 +153,23 @@ class Esp8266BitBangBus : public PixelBus { bool _initialized; // ----------------------------------------------------------------------- - // Shared static state (one context for ALL Esp8266BitBangBus instances) - // All timing fields are static because every BitBang bus must use the - // same LED type (enforced by PixelBusAllocator). - // ESP8266 has only one 32-bit GPIO output register (pins 0–15 usable as output). + // Shared state (one context for ALL Esp8266BitBangBus instances) + // All timing fields must use the same LED type (enforced by PixelBusAllocator). // ----------------------------------------------------------------------- - static int8_t s_pins[WLED_MAX_BB_CHANNELS]; - static uint16_t s_numPixels[WLED_MAX_BB_CHANNELS]; - static uint8_t* s_pixelData[WLED_MAX_BB_CHANNELS]; - static uint8_t s_channelCount; - static uint32_t s_allMask; // GPIO bitmask of all registered output pins - static uint8_t s_stagedCount; // how many channels have called show() this frame - // Timing in CPU cycles — identical across all channels - static uint32_t s_t0h; - static uint32_t s_t1h; - static uint32_t s_period; - static uint8_t s_pixelBytes; // bytes per encoded pixel + struct BBstate { + uint8_t* pixelData[WLED_MAX_BB_CHANNELS]; // 4 bytes each + uint32_t allMask; // GPIO bitmask of all registered output pins + uint32_t t0h; // Timing in CPU cycles — identical across all channels + uint32_t t1h; + uint32_t period; + uint16_t numPixels[WLED_MAX_BB_CHANNELS]; + int8_t pins[WLED_MAX_BB_CHANNELS]; + uint8_t channelCount; + uint8_t stagedCount; // how many channels have called show() this frame + uint8_t pixelBytes; // bytes per encoded pixel + // no trailing padding needed: ends on uint8_t after the above + }; + static BBstate* _BBs; // Core output routine — must run from IRAM for timing accuracy static bool IRAM_ATTR outputParallel(); From 22e1632afcea50d711e2fba599db242a68e7e7a2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 26 Jun 2026 11:59:09 +0200 Subject: [PATCH 160/173] add struct fix also to regular BB, fix UI, add (non working) gamma to ESP8266 path --- wled00/FX_fcn.cpp | 20 +- wled00/bus_manager.cpp | 2 +- wled00/bus_manager.h | 2 +- wled00/data/settings_leds.htm | 4 +- .../src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp | 172 ++++++++---------- .../src/WLEDpixelBus/WLEDpixelBus_BitBang.h | 39 ++-- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 2 +- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 1 - 8 files changed, 115 insertions(+), 127 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 5a6f1030dd..91ac076906 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1788,9 +1788,10 @@ void WS2812FX::show() { 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(); // CCT per-pixel tracking: only needed when multiple active segments have *different* CCT values. @@ -1847,8 +1848,6 @@ void WS2812FX::show() { // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) if (cctFromRgb) BusManager::setSegmentCCT(-1); else if (!_pixelCCT) BusManager::setSegmentCCT(uniformCCT, correctWB); // uniform CCT: set once before loop - // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode - bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); for (size_t i = 0; i < totalLen; i++) { // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then @@ -1866,6 +1865,21 @@ void WS2812FX::show() { p_free(_pixelCCT); _pixelCCT = nullptr; + #else + /* + // TODO: this is not working, something wrong with color encoding/color order and it should use the raw pixel color, maybe move this down to bus level? + if (useGammaCorrection) { + for (auto &bus : BusManager::busses) { + if (bus->isDigital() && bus->isOk()) { + BusDigital &busd = static_cast(*bus); + for (int i = 0; i < busd.getLength(); i++) { + uint32_t c = busd.getPixelColor(i); // TODO: this uses restore color lossy, which is not needed. + if (c > 0) c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss + busd.setPixelColor(i, c); + } + } + } + }*/ #endif // pass the pixels on to the buses and start sending them out diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b015653265..2200134782 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -376,7 +376,7 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; if (_reversed) pix = _len - pix -1; pix += _skip; - const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); // TODO: do we need the color order? where is getpixelcolor used? + //const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); // TODO: do we need the color order? where is getpixelcolor used? uint32_t rawC = _busPtr->getPixelColor(pix); uint32_t c = restoreColorLossy(rawC, _totalBusBri); return c; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index c6e9585111..42304800dc 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -302,7 +302,7 @@ class BusDigital : public Bus { private: uint8_t _skip; - uint8_t _colorOrder; + uint8_t _colorOrder; // TODO: is this still used? color order is now done in bus uint8_t _pins[2] = {255, 255}; uint8_t _driverType; // BusDriverType: BUSDRV_RMT / BUSDRV_I2S / BUSDRV_BITBANG uint16_t _frequencykHz; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 2296c5d200..9815d75cad 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -538,7 +538,7 @@ chanuse.style.display = 'inline'; chanuse.style.color = '#ccc'; channelMsg.textContent = `Hardware channels used: RMT ${usage.rmtUsed}/${maxRMT}, I2S ${usage.i2sUsed}/${maxI2S}` + (usage.bbUsed > 0 ? `, BitBang ${usage.bbUsed}/${maxBB}` : ''); - if (isC3) channelMsg.textContent = channelMsg.textContent.replace("I2S", "SPI"); // replace I2S with "SPI" on C3 + if (isC3()) channelMsg.textContent = channelMsg.textContent.replace("I2S", "SPI"); // replace I2S with "SPI" on C3 if (usage.rmtUsed > maxRMT || usage.i2sUsed > maxI2S || usage.bbUsed > maxBB) { chanuse.style.color = 'red'; } @@ -1108,7 +1108,7 @@ let rmtOpt = drvsel.querySelector('option[value="0"]'); let i2sOpt = drvsel.querySelector('option[value="1"]'); let bbOpt = drvsel.querySelector('option[value="2"]'); - if (isC3) i2sOpt.textContent = "SPI"; // rename to SPI on C3 + if (isC3()) i2sOpt.textContent = "SPI"; // rename to SPI on C3 rmtOpt.disabled = false; i2sOpt.disabled = false; if (bbOpt) bbOpt.disabled = false; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp index 549b56b04e..461420ed81 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -15,25 +15,8 @@ Each bus can have individual configuration of color channels but all must share #include "esp_clk.h" // esp_clk_cpu_freq() namespace WLEDpixelBus { - -// --------------------------------------------------------------------------- -// Static member definitions -// --------------------------------------------------------------------------- -int8_t BitBangBus::s_pins[WLED_MAX_BB_CHANNELS] = {}; -uint16_t BitBangBus::s_numPixels[WLED_MAX_BB_CHANNELS] = {}; -uint8_t* BitBangBus::s_pixelData[WLED_MAX_BB_CHANNELS] = {}; -uint8_t BitBangBus::s_channelCount = 0; -uint32_t BitBangBus::s_allMask = 0; -#ifdef ESP_HAS_HIGH_GPIO_BANK -uint32_t BitBangBus::s_allMaskHigh = 0; -#endif -uint8_t BitBangBus::s_stagedCount = 0; -portMUX_TYPE BitBangBus::s_mux = portMUX_INITIALIZER_UNLOCKED; -uint32_t BitBangBus::s_t0h = 0; -uint32_t BitBangBus::s_t1h = 0; -uint32_t BitBangBus::s_period = 0; -uint32_t BitBangBus::s_latchCycles = 0; -uint8_t BitBangBus::s_pixelBytes = 0; +// BB state, shared among all buses +BitBangBus::BBstate* BitBangBus::_BBs = nullptr; // --------------------------------------------------------------------------- // getCycleCount() — read the CPU cycle counter. @@ -88,8 +71,13 @@ bool BitBangBus::begin() { // SOC_GPIO_VALID_OUTPUT_GPIO_MASK — 64-bit bitmask of GPIOs usable as outputs // (excludes input-only pins such as GPIO 34-39 on ESP32). // High bank (GPIO_OUT1) is needed for pins > 31 on ESP32/S2/S3. - if (_pin < 0 || _pin >= SOC_GPIO_PIN_COUNT || - !((SOC_GPIO_VALID_OUTPUT_GPIO_MASK >> _pin) & 1ULL)) return false; + if (_pin < 0 || _pin >= SOC_GPIO_PIN_COUNT || !((SOC_GPIO_VALID_OUTPUT_GPIO_MASK >> _pin) & 1ULL)) return false; + + if (!_BBs) { + _BBs = (BBstate*)calloc(1, sizeof(BBstate)); // Allocate shared state struct on first channel + if (!_BBs) return false; + } + if (_BBs->channelCount >= WLED_MAX_BB_CHANNELS) return false; // Configure GPIO as output, drive LOW (idle state for non-inverted LEDs) gpio_set_direction((gpio_num_t)_pin, GPIO_MODE_OUTPUT); @@ -111,25 +99,24 @@ bool BitBangBus::begin() { const uint32_t latchCycles = (uint32_t)_rawTiming.reset_us * cpuMHz; // Register in the shared static table - if (s_channelCount >= WLED_MAX_BB_CHANNELS) return false; - const uint8_t idx = s_channelCount; - s_pins[idx] = _pin; + const uint8_t idx = _BBs->channelCount; + _BBs->pins[idx] = _pin; #ifdef ESP_HAS_HIGH_GPIO_BANK - if (_pin >= 32) s_allMaskHigh |= (1u << (_pin - 32)); - else s_allMask |= (1u << _pin); + if (_pin >= 32) _BBs->allMaskHigh |= (1u << (_pin - 32)); + else _BBs->allMask |= (1u << _pin); #else - s_allMask |= (1u << _pin); + _BBs->allMask |= (1u << _pin); #endif - s_channelCount++; + _BBs->channelCount++; // Allocate the per-pixel encode buffer (via PixelBus helper) if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { - s_channelCount--; + _BBs->channelCount--; #ifdef ESP_HAS_HIGH_GPIO_BANK - if (_pin >= 32) s_allMaskHigh &= ~(1u << (_pin - 32)); - else s_allMask &= ~(1u << _pin); + if (_pin >= 32) _BBs->allMaskHigh &= ~(1u << (_pin - 32)); + else _BBs->allMask &= ~(1u << _pin); #else - s_allMask &= ~(1u << _pin); + _BBs->allMask &= ~(1u << _pin); #endif return false; } @@ -137,13 +124,13 @@ bool BitBangBus::begin() { // Publish per-channel data pointers into the shared static arrays. // Timing is set here (same for all channels; overwriting with same values is harmless). - s_numPixels[idx] = _numPixels; - s_pixelData[idx] = _pixelData; - s_t0h = t0h; - s_t1h = t1h; - s_period = period; - s_latchCycles = latchCycles; - s_pixelBytes = _encoder.getPixelBytes(); + _BBs->numPixels[idx] = _numPixels; + _BBs->pixelData[idx] = _pixelData; + _BBs->t0h = t0h; + _BBs->t1h = t1h; + _BBs->period = period; + _BBs->latchCycles = latchCycles; + _BBs->pixelBytes = _encoder.getPixelBytes(); _initialized = true; return true; @@ -156,30 +143,30 @@ void BitBangBus::setInverted(bool inv) { void BitBangBus::end() { if (_initialized) { - // Find our slot by scanning s_pins (no stored index needed). - uint8_t slot = s_channelCount; // sentinel: not found - for (uint8_t i = 0; i < s_channelCount; i++) { - if (s_pins[i] == _pin) { slot = i; break; } + // Find our slot by scanning _BBs->pins (no stored index needed). + uint8_t slot = _BBs->channelCount; // sentinel: not found + for (uint8_t i = 0; i < _BBs->channelCount; i++) { + if (_BBs->pins[i] == _pin) { slot = i; break; } } - if (slot < s_channelCount) { + if (slot < _BBs->channelCount) { // Shift remaining entries down to fill the gap. - for (uint8_t i = slot; i + 1 < s_channelCount; i++) { - s_pins[i] = s_pins[i + 1]; - s_numPixels[i] = s_numPixels[i + 1]; - s_pixelData[i] = s_pixelData[i + 1]; + for (uint8_t i = slot; i + 1 < _BBs->channelCount; i++) { + _BBs->pins[i] = _BBs->pins[i + 1]; + _BBs->numPixels[i] = _BBs->numPixels[i + 1]; + _BBs->pixelData[i] = _BBs->pixelData[i + 1]; } - s_channelCount--; - s_allMask = 0; + _BBs->channelCount--; + _BBs->allMask = 0; #ifdef ESP_HAS_HIGH_GPIO_BANK - s_allMaskHigh = 0; - for (uint8_t i = 0; i < s_channelCount; i++) { - if (s_pins[i] >= 32) s_allMaskHigh |= (1u << (s_pins[i] - 32)); - else s_allMask |= (1u << s_pins[i]); + _BBs->allMaskHigh = 0; + for (uint8_t i = 0; i < _BBs->channelCount; i++) { + if (_BBs->pins[i] >= 32) _BBs->allMaskHigh |= (1u << (_BBs->pins[i] - 32)); + else _BBs->allMask |= (1u << _BBs->pins[i]); } #else - for (uint8_t i = 0; i < s_channelCount; i++) s_allMask |= (1u << s_pins[i]); + for (uint8_t i = 0; i < _BBs->channelCount; i++) _BBs->allMask |= (1u << _BBs->pins[i]); #endif - s_stagedCount = 0; + _BBs->stagedCount = 0; } } _initialized = false; @@ -201,34 +188,21 @@ void BitBangBus::end() { bool BitBangBus::show(const uint32_t*, uint16_t, const CctPixel*) { if (!_initialized || !_pixelData) return false; - s_stagedCount++; - if (s_stagedCount < s_channelCount) { + _BBs->stagedCount++; + if (_BBs->stagedCount < _BBs->channelCount) { return true; // not all channels ready yet } - // Last channel staged — output all channels in parallel - s_stagedCount = 0; - return outputParallel(); + _BBs->stagedCount = 0; // reset for next frame + return outputParallel(); // send data to LEDs (blocking) + } // --------------------------------------------------------------------------- // resetChannels() — called by PixelBusAllocator::resetChannelTracking() // --------------------------------------------------------------------------- void BitBangBus::resetChannels() { - s_channelCount = 0; - s_allMask = 0; -#ifdef ESP_HAS_HIGH_GPIO_BANK - s_allMaskHigh = 0; -#endif - s_stagedCount = 0; - s_t0h = 0; - s_t1h = 0; - s_period = 0; - s_latchCycles = 0; - s_pixelBytes = 0; - memset(s_pins, 0, sizeof(s_pins)); - memset(s_numPixels, 0, sizeof(s_numPixels)); - memset(s_pixelData, 0, sizeof(s_pixelData)); + if (_BBs) memset(_BBs, 0, sizeof(BBstate)); } // --------------------------------------------------------------------------- @@ -249,27 +223,27 @@ void BitBangBus::resetChannels() { // have fewer pixels than maxPixels simply output '0' bits once their data ends. // --------------------------------------------------------------------------- bool IRAM_ATTR BitBangBus::outputParallel() { - if (s_channelCount == 0) return true; - - const uint32_t t0h = s_t0h; - const uint32_t t1h = s_t1h; - const uint32_t period = s_period; - const uint32_t latchCycles = s_latchCycles; - const uint8_t pixelBytes = s_pixelBytes; - const uint8_t nCh = s_channelCount; + if (!_BBs || _BBs->channelCount == 0) return true; + + const uint32_t t0h = _BBs->t0h; + const uint32_t t1h = _BBs->t1h; + const uint32_t period = _BBs->period; + const uint32_t latchCycles = _BBs->latchCycles; + const uint8_t pixelBytes = _BBs->pixelBytes; + const uint8_t nCh = _BBs->channelCount; // GPIO output masks — split into low bank (pins 0–31, all variants) and // high bank (pins 32+, ESP32/S2/S3 only via GPIO_OUT1_W1TS/TC_REG). #ifdef ESP_HAS_HIGH_GPIO_BANK - const uint32_t setOutputMaskLow = s_allMask; // pins 0–31 - const uint32_t setOutputMaskHigh = s_allMaskHigh; // pins 32+ + const uint32_t setOutputMaskLow = _BBs->allMask; // pins 0–31 + const uint32_t setOutputMaskHigh = _BBs->allMaskHigh; // pins 32+ #else - const uint32_t setOutputMask = s_allMask; // GPIO bitmask of all active output pins (0–31) + const uint32_t setOutputMask = _BBs->allMask; // GPIO bitmask of all active output pins (0–31) #endif // Find the maximum pixel count across all channels (drives total loop length). uint16_t maxPixels = 0; for (uint8_t ch = 0; ch < nCh; ch++) { - if (s_numPixels[ch] > maxPixels) maxPixels = s_numPixels[ch]; + if (_BBs->numPixels[ch] > maxPixels) maxPixels = _BBs->numPixels[ch]; } if (maxPixels == 0) return true; @@ -285,17 +259,17 @@ bool IRAM_ATTR BitBangBus::outputParallel() { uint32_t chanTotalBytes[nCh]; for (uint8_t ch = 0; ch < nCh; ch++) { #ifdef ESP_HAS_HIGH_GPIO_BANK - if (s_pins[ch] >= 32) { + if (_BBs->pins[ch] >= 32) { chanPinMask[ch] = 0; - chanPinMaskHigh[ch] = 1u << (s_pins[ch] - 32); + chanPinMaskHigh[ch] = 1u << (_BBs->pins[ch] - 32); } else { - chanPinMask[ch] = 1u << s_pins[ch]; + chanPinMask[ch] = 1u << _BBs->pins[ch]; chanPinMaskHigh[ch] = 0; } #else - chanPinMask[ch] = 1u << s_pins[ch]; + chanPinMask[ch] = 1u << _BBs->pins[ch]; #endif - chanTotalBytes[ch] = (uint32_t)s_numPixels[ch] * pixelBytes; + chanTotalBytes[ch] = (uint32_t)_BBs->numPixels[ch] * pixelBytes; } // Returns the GPIO mask(s) of all channels that should output a logical '0' for @@ -308,7 +282,7 @@ bool IRAM_ATTR BitBangBus::outputParallel() { zmLow = 0; zmHigh = 0; for (uint8_t ch = 0; ch < nCh; ch++) { if (byteIndex >= chanTotalBytes[ch] || - !(s_pixelData[ch][byteIndex] & (1u << bitPos))) { + !(_BBs->pixelData[ch][byteIndex] & (1u << bitPos))) { zmLow |= chanPinMask[ch]; zmHigh |= chanPinMaskHigh[ch]; } @@ -321,7 +295,7 @@ bool IRAM_ATTR BitBangBus::outputParallel() { uint32_t zm = 0; for (uint8_t ch = 0; ch < nCh; ch++) { if (byteIndex >= chanTotalBytes[ch] || - !(s_pixelData[ch][byteIndex] & (1u << bitPos))) { + !(_BBs->pixelData[ch][byteIndex] & (1u << bitPos))) { zm |= chanPinMask[ch]; } } @@ -332,7 +306,7 @@ bool IRAM_ATTR BitBangBus::outputParallel() { // Period reference: initialise as already expired so the first pulse fires immediately. uint32_t cyclesStart = getCycleCount() - period; - portENTER_CRITICAL(&s_mux); + portENTER_CRITICAL(&_BBs->mux); for (uint32_t bitIndex = 0; bitIndex < totalBits; bitIndex++) { @@ -350,10 +324,10 @@ bool IRAM_ATTR BitBangBus::outputParallel() { // note: this can lead to glitches, so far nothing bad was detected when fully blocking for up to 140ms so this is probably unnecessary (might be problematic for ESPNow, need to check) /* if (bitIndex > 0 && (bitIndex % bitsPerPixel) == 0) { - portEXIT_CRITICAL(&s_mux); - portENTER_CRITICAL(&s_mux); + portEXIT_CRITICAL(&_BBs->mux); + portENTER_CRITICAL(&_BBs->mux); if (latchCycles > 0 && (getCycleCount() - cyclesStart) > latchCycles) { - portEXIT_CRITICAL(&s_mux); + portEXIT_CRITICAL(&_BBs->mux); return false; // ISR latency caused accidental LED latch — frame aborted } }*/ @@ -389,7 +363,7 @@ bool IRAM_ATTR BitBangBus::outputParallel() { #endif } - portEXIT_CRITICAL(&s_mux); + portEXIT_CRITICAL(&_BBs->mux); return true; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h index 8bac9357b1..fbab202e12 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -60,26 +60,27 @@ class BitBangBus : public PixelBus { LedTiming _rawTiming; // saved at construction, converted to cycles in begin() // ----------------------------------------------------------------------- - // Shared static state (one context for ALL BitBangBus instances) - // All timing fields are static because every BitBang bus must use the - // same LED type (enforced by PixelBusAllocator). + // Shared state (one context for ALL BitBangBus instances) + // All timing fields must use the same LED type (enforced by PixelBusAllocator). // ----------------------------------------------------------------------- - static int8_t s_pins[WLED_MAX_BB_CHANNELS]; - static uint16_t s_numPixels[WLED_MAX_BB_CHANNELS]; // pixel count per channel - static uint8_t* s_pixelData[WLED_MAX_BB_CHANNELS]; // encoded data pointer per channel - static uint8_t s_channelCount; - static uint32_t s_allMask; // GPIO bitmask of registered pins 0–31 -#ifdef ESP_HAS_HIGH_GPIO_BANK - static uint32_t s_allMaskHigh; // GPIO bitmask of registered pins 32+ (ESP32/S2/S3) -#endif - static uint8_t s_stagedCount; // how many channels have called show() this frame - static portMUX_TYPE s_mux; // critical-section lock used inside outputParallel() - // Timing in CPU cycles — identical across all channels - static uint32_t s_t0h; - static uint32_t s_t1h; - static uint32_t s_period; - static uint32_t s_latchCycles; - static uint8_t s_pixelBytes; // bytes per encoded pixel (derived from LED type) + struct BBstate { + uint8_t* pixelData[WLED_MAX_BB_CHANNELS]; // encoded data pointer per channel + uint32_t t0h; // Timing in CPU cycles — identical across all channels + uint32_t t1h; + uint32_t period; + uint32_t latchCycles; + uint32_t allMask; // GPIO bitmask of registered pins 0–31 + #ifdef ESP_HAS_HIGH_GPIO_BANK + uint32_t allMaskHigh; // GPIO bitmask of registered pins 32+ (ESP32/S2/S3) + #endif + portMUX_TYPE mux; // critical-section lock used inside outputParallel() + int8_t pins[WLED_MAX_BB_CHANNELS]; + uint16_t numPixels[WLED_MAX_BB_CHANNELS]; // pixel count per channel + uint8_t channelCount; + uint8_t stagedCount; // how many channels have called show() this frame + uint8_t pixelBytes; // bytes per encoded pixel (derived from LED type) + }; + static BBstate* _BBs; // ----------------------------------------------------------------------- // Core output routine — must run from IRAM for timing accuracy diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index 5fbb687a34..cec0cce7ec 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -295,7 +295,7 @@ IRAM_ATTR bool Esp8266BitBangBus::show(const uint32_t* /*pixels*/, uint16_t /*nu } // Last channel staged — output all channels in parallel - _BBs->stagedCount = 0; // reset for next frame + _BBs->stagedCount = 0; // reset for next frame return outputParallel(); // send data to LEDs (blocking) } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index cf4af13679..390ccfc556 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -167,7 +167,6 @@ class Esp8266BitBangBus : public PixelBus { uint8_t channelCount; uint8_t stagedCount; // how many channels have called show() this frame uint8_t pixelBytes; // bytes per encoded pixel - // no trailing padding needed: ends on uint8_t after the above }; static BBstate* _BBs; From 4c8cabc38a6e9dce9c349ff33022d53ae2673f9d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 27 Jun 2026 14:18:27 +0200 Subject: [PATCH 161/173] remove ESP8266 BB bus and extend BB bus to support ESP8266 --- wled00/bus_wrapper.h | 3 +- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 1 + .../src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp | 99 +++++--- .../src/WLEDpixelBus/WLEDpixelBus_BitBang.h | 13 +- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp | 215 ------------------ .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 51 ----- 7 files changed, 78 insertions(+), 306 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index b686a6ce13..f73f78200e 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -12,6 +12,7 @@ #include "src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h" #elif defined(ARDUINO_ARCH_ESP8266) #include "src/WLEDpixelBus/WLEDpixelBus_ESP8266.h" +#include "src/WLEDpixelBus/WLEDpixelBus_BitBang.h" #endif //Hardware SPI Pins (ESP8266 only; ESP32 uses bus allocation order to detect HSPI) @@ -54,7 +55,7 @@ class PixelBusAllocator { #endif #else _bitBangBusType = 0; // TYPE_NONE - WLEDpixelBus::Esp8266BitBangBus::resetChannels(); + WLEDpixelBus::BitBangBus::resetChannels(); #endif } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 0dd962d98b..a6297c8d40 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -251,7 +251,7 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 bus = new Esp8266DmaBus(pin, timing, colorOrder, numChannels, ledType); break; case BusDriver::BitBang: - bus = new Esp8266BitBangBus(pin, timing, colorOrder, numChannels, ledType); + bus = new BitBangBus(pin, timing, colorOrder, numChannels, ledType); break; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index aec76e28a8..0416a26e51 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -575,5 +575,6 @@ PixelBus* createBus(BusDriver type, int8_t pin, const LedTiming& timing, #include "WLEDpixelBus_BitBang.h" #elif defined(ESP8266) #include "WLEDpixelBus_ESP8266.h" +#include "WLEDpixelBus_BitBang.h" #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp index 461420ed81..b680799be8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -10,9 +10,15 @@ Each bus can have individual configuration of color channels but all must share -------------------------------------------------------------------------*/ -#if defined(ARDUINO_ARCH_ESP32) #include "WLEDpixelBus_BitBang.h" +#if defined(ARDUINO_ARCH_ESP32) #include "esp_clk.h" // esp_clk_cpu_freq() +#elif defined(ESP8266) +#include +#define REG_WRITE(addr, val) GPIO_REG_WRITE(addr, val) +#define GPIO_OUT_W1TS_REG GPIO_OUT_W1TS_ADDRESS +#define GPIO_OUT_W1TC_REG GPIO_OUT_W1TC_ADDRESS +#endif namespace WLEDpixelBus { // BB state, shared among all buses @@ -22,7 +28,14 @@ BitBangBus::BBstate* BitBangBus::_BBs = nullptr; // getCycleCount() — read the CPU cycle counter. // Must be IRAM_ATTR because it is called from outputParallel() which is IRAM. // --------------------------------------------------------------------------- -#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || \ +#if defined(ESP8266) + static inline uint32_t IRAM_ATTR getCycleCount() { + uint32_t ccount; + __asm__ __volatile__("rsr %0,ccount" : "=a"(ccount)); + return ccount; + } + static constexpr uint32_t LOOPTEST_CYCLES = 1; +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || \ defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32P4) // RISC-V: machine cycle counter CSR 0x7e2 (vendor extension, same as NeoPixelBus) static inline uint32_t IRAM_ATTR getCycleCount() { @@ -66,12 +79,12 @@ BitBangBus::~BitBangBus() { bool BitBangBus::begin() { if (_initialized) return true; - // GPIO pin range check using IDF macros from soc/soc_caps.h: - // SOC_GPIO_PIN_COUNT — total GPIO count for this target (40/ESP32, 47/S2, 49/S3, 22/C3, ...). - // SOC_GPIO_VALID_OUTPUT_GPIO_MASK — 64-bit bitmask of GPIOs usable as outputs - // (excludes input-only pins such as GPIO 34-39 on ESP32). - // High bank (GPIO_OUT1) is needed for pins > 31 on ESP32/S2/S3. + // GPIO pin range check +#if defined(ARDUINO_ARCH_ESP32) if (_pin < 0 || _pin >= SOC_GPIO_PIN_COUNT || !((SOC_GPIO_VALID_OUTPUT_GPIO_MASK >> _pin) & 1ULL)) return false; +#elif defined(ESP8266) + if (_pin < 0 || _pin > 15) return false; +#endif if (!_BBs) { _BBs = (BBstate*)calloc(1, sizeof(BBstate)); // Allocate shared state struct on first channel @@ -80,11 +93,20 @@ bool BitBangBus::begin() { if (_BBs->channelCount >= WLED_MAX_BB_CHANNELS) return false; // Configure GPIO as output, drive LOW (idle state for non-inverted LEDs) +#if defined(ARDUINO_ARCH_ESP32) gpio_set_direction((gpio_num_t)_pin, GPIO_MODE_OUTPUT); gpio_set_level((gpio_num_t)_pin, 0); +#elif defined(ESP8266) + pinMode(_pin, OUTPUT); + digitalWrite(_pin, LOW); +#endif // Convert nanosecond timings to CPU cycles +#if defined(ARDUINO_ARCH_ESP32) const uint32_t cpuMHz = esp_clk_cpu_freq() / 1000000u; +#elif defined(ESP8266) + const uint32_t cpuMHz = ESP.getCpuFreqMHz(); +#endif // Subtract the while-loop test overhead (one extra iteration) to compensate // for the latency between the condition passing and the actual GPIO write. @@ -120,7 +142,9 @@ bool BitBangBus::begin() { #endif return false; } +#if defined(ARDUINO_ARCH_ESP32) esp_rom_gpio_connect_out_signal(_pin, SIG_GPIO_OUT_IDX, _inverted, false); // route output pin, inverts signal in hardware if needed +#endif // Publish per-channel data pointers into the shared static arrays. // Timing is set here (same for all channels; overwriting with same values is harmless). @@ -171,7 +195,11 @@ void BitBangBus::end() { } _initialized = false; +#if defined(ARDUINO_ARCH_ESP32) if (_pin >= 0) gpio_reset_pin((gpio_num_t)_pin); // reset all pin settings, including inversion +#elif defined(ESP8266) + if (_pin >= 0) pinMode(_pin, INPUT); +#endif // Release the encode buffer via base class helper. if (_encodeBuffer) { @@ -179,6 +207,12 @@ void BitBangBus::end() { _encodeBuffer = nullptr; _pixelData = nullptr; } + + // If no channels left, free the shared state struct + if (_BBs && _BBs->channelCount == 0) { + free(_BBs); + _BBs = nullptr; + } } // --------------------------------------------------------------------------- @@ -225,6 +259,8 @@ void BitBangBus::resetChannels() { bool IRAM_ATTR BitBangBus::outputParallel() { if (!_BBs || _BBs->channelCount == 0) return true; + uint32_t starttime = micros(); + const uint32_t t0h = _BBs->t0h; const uint32_t t1h = _BBs->t1h; const uint32_t period = _BBs->period; @@ -278,11 +314,10 @@ bool IRAM_ATTR BitBangBus::outputParallel() { #ifdef ESP_HAS_HIGH_GPIO_BANK auto computeZeroMasks = [&](uint32_t bitIndex, uint32_t& zmLow, uint32_t& zmHigh) { const uint32_t byteIndex = bitIndex >> 3; - const uint8_t bitPos = 7u - (uint8_t)(bitIndex & 7u); // MSB first + const uint8_t bitMask = 0x80u >> (bitIndex & 7u); zmLow = 0; zmHigh = 0; for (uint8_t ch = 0; ch < nCh; ch++) { - if (byteIndex >= chanTotalBytes[ch] || - !(_BBs->pixelData[ch][byteIndex] & (1u << bitPos))) { + if (byteIndex >= chanTotalBytes[ch] || !(_BBs->pixelData[ch][byteIndex] & bitMask)) { zmLow |= chanPinMask[ch]; zmHigh |= chanPinMaskHigh[ch]; } @@ -291,11 +326,10 @@ bool IRAM_ATTR BitBangBus::outputParallel() { #else auto computeZeroMask = [&](uint32_t bitIndex) -> uint32_t { const uint32_t byteIndex = bitIndex >> 3; - const uint8_t bitPos = 7u - (uint8_t)(bitIndex & 7u); // MSB first + const uint8_t bitMask = 0x80u >> (bitIndex & 7u); uint32_t zm = 0; for (uint8_t ch = 0; ch < nCh; ch++) { - if (byteIndex >= chanTotalBytes[ch] || - !(_BBs->pixelData[ch][byteIndex] & (1u << bitPos))) { + if (byteIndex >= chanTotalBytes[ch] || !(_BBs->pixelData[ch][byteIndex] & bitMask)) { zm |= chanPinMask[ch]; } } @@ -306,11 +340,15 @@ bool IRAM_ATTR BitBangBus::outputParallel() { // Period reference: initialise as already expired so the first pulse fires immediately. uint32_t cyclesStart = getCycleCount() - period; +#if defined(ARDUINO_ARCH_ESP32) portENTER_CRITICAL(&_BBs->mux); +#elif defined(ESP8266) + os_intr_lock(); +#endif for (uint32_t bitIndex = 0; bitIndex < totalBits; bitIndex++) { - // ── Step 1: Compute zero mask(s) for this bit ──────────────────────── + // Compute zero mask(s) for this bit #ifdef ESP_HAS_HIGH_GPIO_BANK uint32_t zeroMaskLow = 0, zeroMaskHigh = 0; computeZeroMasks(bitIndex, zeroMaskLow, zeroMaskHigh); @@ -318,24 +356,10 @@ bool IRAM_ATTR BitBangBus::outputParallel() { uint32_t zeroMask = computeZeroMask(bitIndex); #endif - // Pixel boundary — release ISR lock, check latch ─────────── - // On every pixel boundary (except the very first bit) give ISRs a chance - // to run, then verify the idle gap has not caused an accidental LED latch. - // note: this can lead to glitches, so far nothing bad was detected when fully blocking for up to 140ms so this is probably unnecessary (might be problematic for ESPNow, need to check) - /* - if (bitIndex > 0 && (bitIndex % bitsPerPixel) == 0) { - portEXIT_CRITICAL(&_BBs->mux); - portENTER_CRITICAL(&_BBs->mux); - if (latchCycles > 0 && (getCycleCount() - cyclesStart) > latchCycles) { - portEXIT_CRITICAL(&_BBs->mux); - return false; // ISR latency caused accidental LED latch — frame aborted - } - }*/ - - // Wait for the full bit period since the last HIGH edge ────── + // Wait for the full bit period since the last HIGH edge while ((getCycleCount() - cyclesStart) < period); - // ── Step 4: Set all outputs HIGH simultaneously ─────────────────────── + // Set all outputs HIGH simultaneously #ifdef ESP_HAS_HIGH_GPIO_BANK REG_WRITE(GPIO_OUT_W1TS_REG, setOutputMaskLow); REG_WRITE(GPIO_OUT1_W1TS_REG, setOutputMaskHigh); @@ -344,7 +368,7 @@ bool IRAM_ATTR BitBangBus::outputParallel() { #endif cyclesStart = getCycleCount(); - // ── Step 5: After T0H — pull '0' outputs LOW ───────────────────────── + // After T0H — pull '0' outputs LOW while ((getCycleCount() - cyclesStart) < t0h); #ifdef ESP_HAS_HIGH_GPIO_BANK REG_WRITE(GPIO_OUT_W1TC_REG, zeroMaskLow); @@ -353,7 +377,7 @@ bool IRAM_ATTR BitBangBus::outputParallel() { REG_WRITE(GPIO_OUT_W1TC_REG, zeroMask); #endif - // ── Step 6: After T1H — pull all remaining outputs LOW ─────────────── + // After T1H — pull all remaining outputs LOW while ((getCycleCount() - cyclesStart) < t1h); #ifdef ESP_HAS_HIGH_GPIO_BANK REG_WRITE(GPIO_OUT_W1TC_REG, setOutputMaskLow); @@ -363,10 +387,17 @@ bool IRAM_ATTR BitBangBus::outputParallel() { #endif } +#if defined(ARDUINO_ARCH_ESP32) portEXIT_CRITICAL(&_BBs->mux); +#elif defined(ESP8266) + os_intr_unlock(); +#endif + + uint32_t endtime = micros(); + Serial.printf("took %d us\n", endtime - starttime); + return true; } -} // namespace WLEDpixelBus -#endif // ARDUINO_ARCH_ESP32 +} // namespace WLEDpixelBus \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h index fbab202e12..4e845724d3 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.h @@ -12,14 +12,19 @@ Each bus can have individual configuration of color channels but all must share #pragma once -#if defined(ARDUINO_ARCH_ESP32) - #include "WLEDpixelBus.h" +#if defined(ARDUINO_ARCH_ESP32) #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" //#include "soc/gpio_struct.h" // GPIO.out_w1ts / GPIO.out_w1tc -> better use REG_WRITE #include "soc/gpio_reg.h" #include "driver/gpio.h" +#elif defined(ESP8266) +#include +#include +#include +#include +#endif namespace WLEDpixelBus { @@ -73,7 +78,9 @@ class BitBangBus : public PixelBus { #ifdef ESP_HAS_HIGH_GPIO_BANK uint32_t allMaskHigh; // GPIO bitmask of registered pins 32+ (ESP32/S2/S3) #endif + #if defined(ARDUINO_ARCH_ESP32) portMUX_TYPE mux; // critical-section lock used inside outputParallel() + #endif int8_t pins[WLED_MAX_BB_CHANNELS]; uint16_t numPixels[WLED_MAX_BB_CHANNELS]; // pixel count per channel uint8_t channelCount; @@ -89,5 +96,3 @@ class BitBangBus : public PixelBus { }; } // namespace WLEDpixelBus - -#endif // ARDUINO_ARCH_ESP32 diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp index cec0cce7ec..c174952979 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.cpp @@ -173,222 +173,7 @@ bool Esp8266UartBus::canShow() const { } -//============================================================================== -// ESP8266 BitBang Bus (parallel output across multiple pins) -//============================================================================== - -Esp8266BitBangBus::BBstate* Esp8266BitBangBus::_BBs = nullptr; - -Esp8266BitBangBus::Esp8266BitBangBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) - : _pin(pin), _timing(timing), _initialized(false) { - _encoder = ColorEncoder(colorOrder, numChannels, ledType); - _ledType = ledType; -} - -Esp8266BitBangBus::~Esp8266BitBangBus() { - end(); -} - -bool Esp8266BitBangBus::begin() { - if (_initialized) return true; - if (_pin < 0 || _pin > 15) return false; // Only GPIO 0-15 are output-capable on ESP8266 - if (!_BBs) { - _BBs = (BBstate*)calloc(1, sizeof(BBstate)); // Allocate shared state struct on first channel - if (!_BBs) return false; - } - if (_BBs->channelCount >= WLED_MAX_BB_CHANNELS) return false; - - pinMode(_pin, OUTPUT); - digitalWrite(_pin, LOW); - - // Convert nanosecond timings to CPU cycles. - // Subtract 1 cycle for the while-loop test overhead (same correction as ESP32). - const uint32_t cpuMHz = ESP.getCpuFreqMHz(); // 80 or 160 - - uint32_t t0h = (_timing.t0h_ns * cpuMHz) / 1000u; - uint32_t t1h = (_timing.t1h_ns * cpuMHz) / 1000u; - t0h = (t0h > 1u) ? t0h - 1u : 0u; - t1h = (t1h > 1u) ? t1h - 1u : 0u; - uint32_t period = (_timing.bitPeriod() * cpuMHz) / 1000u; - period = (period > 1u) ? period - 1u : 0u; - - // Register in the shared state table - const uint8_t idx = _BBs->channelCount; - _BBs->pins[idx] = _pin; - _BBs->allMask |= (1u << _pin); - _BBs->channelCount++; - - if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { - _BBs->channelCount--; - _BBs->allMask &= ~(1u << _pin); - return false; - } - - _BBs->numPixels[idx] = _numPixels; - _BBs->pixelData[idx] = _pixelData; - _BBs->t0h = t0h; // Timing is shared — same for all channels (enforced by PixelBusAllocator). - _BBs->t1h = t1h; - _BBs->period = period; - _BBs->pixelBytes = _encoder.getPixelBytes(); - - _initialized = true; - return true; -} - -void Esp8266BitBangBus::end() { - if (_initialized && _BBs) { - uint8_t slot = _BBs->channelCount; - for (uint8_t i = 0; i < _BBs->channelCount; i++) { - if (_BBs->pins[i] == _pin) { slot = i; break; } - } - if (slot < _BBs->channelCount) { - for (uint8_t i = slot; i + 1 < _BBs->channelCount; i++) { - _BBs->pins[i] = _BBs->pins[i + 1]; - _BBs->numPixels[i] = _BBs->numPixels[i + 1]; - _BBs->pixelData[i] = _BBs->pixelData[i + 1]; - } - _BBs->channelCount--; - _BBs->allMask = 0; - for (uint8_t i = 0; i < _BBs->channelCount; i++) - _BBs->allMask |= (1u << _BBs->pins[i]); - _BBs->stagedCount = 0; - } - if (_BBs->channelCount == 0) { - free(_BBs); - _BBs = nullptr; - } - } - pinMode(_pin, INPUT); - _initialized = false; - - if (_encodeBuffer) { - free(_encodeBuffer); - _encodeBuffer = nullptr; - _pixelData = nullptr; - } -} - -void Esp8266BitBangBus::setTiming(const LedTiming& timing) { - _timing = timing; - // Recompute cycle counts if already initialized - if (_initialized && _BBs) { - const uint32_t cpuMHz = ESP.getCpuFreqMHz(); - uint32_t t0h = (_timing.t0h_ns * cpuMHz) / 1000u; - uint32_t t1h = (_timing.t1h_ns * cpuMHz) / 1000u; - t0h = (t0h > 1u) ? t0h - 1u : 0u; - t1h = (t1h > 1u) ? t1h - 1u : 0u; - uint32_t period = (_timing.bitPeriod() * cpuMHz) / 1000u; - period = (period > 1u) ? period - 1u : 0u; - _BBs->t0h = t0h; - _BBs->t1h = t1h; - _BBs->period = period; - } -} -// Stage this channel's data. When all channels have staged, output all in parallel. -IRAM_ATTR bool Esp8266BitBangBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { - if (!_initialized || !_pixelData) return false; - - _BBs->stagedCount++; - if (_BBs->stagedCount < _BBs->channelCount) { - return true; // not all channels ready yet - } - - // Last channel staged — output all channels in parallel - _BBs->stagedCount = 0; // reset for next frame - return outputParallel(); // send data to LEDs (blocking) -} - -void Esp8266BitBangBus::setColorOrder(uint8_t co) { - _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); -} - -// --------------------------------------------------------------------------- -// resetChannels() — called by PixelBusAllocator::resetChannelTracking() -// --------------------------------------------------------------------------- -void Esp8266BitBangBus::resetChannels() { - if (_BBs) memset(_BBs, 0, sizeof(BBstate)); -} - -// --------------------------------------------------------------------------- -// outputParallel() — the hot path, must live in IRAM. -// -// Per-bit sequence (identical to ESP32 BitBangBus, single GPIO bank): -// 1. Compute zeroMask for the current bit across all channels. -// 2. Wait for the full bit period since the previous HIGH edge. -// 3. Set all output pins HIGH simultaneously. -// 4. After T0H cycles: pull '0' outputs LOW. -// 5. After T1H cycles: pull all remaining outputs LOW. -// -// Interrupts are locked for the full duration using os_intr_lock() / -// os_intr_unlock() (the SDK equivalent of portENTER/EXIT_CRITICAL on ESP8266). -// The total lock duration equals the previous sequential approach for a single -// channel; for N channels it is identical (all clocked in parallel, same time). -// --------------------------------------------------------------------------- -bool IRAM_ATTR Esp8266BitBangBus::outputParallel() { - if (!_BBs || _BBs->channelCount == 0) return true; - - const uint32_t t0h = _BBs->t0h; - const uint32_t t1h = _BBs->t1h; - const uint32_t period = _BBs->period; - const uint32_t setAllMask = _BBs->allMask; - const uint8_t pixelBytes = _BBs->pixelBytes; - const uint8_t nCh = _BBs->channelCount; - - // Find maximum pixel count across all channels - uint16_t maxPixels = 0; - for (uint8_t ch = 0; ch < nCh; ch++) { - if (_BBs->numPixels[ch] > maxPixels) maxPixels =_BBs->numPixels[ch]; - } - if (maxPixels == 0) return true; - - const uint32_t totalBits = (uint32_t)maxPixels * pixelBytes * 8u; - - // Pre-compute per-channel pin masks and total byte extents - uint32_t chanPinMask[nCh]; - uint32_t chanTotalBytes[nCh]; - for (uint8_t ch = 0; ch < nCh; ch++) { - chanPinMask[ch] = 1u << _BBs->pins[ch]; - chanTotalBytes[ch] = (uint32_t)_BBs->numPixels[ch] * pixelBytes; - } - - // Inline lambda equivalent: compute bitmask of channels outputting '0' for this bit - // Channels past their data end also output '0'. MSB-first within each byte. - // TODO: there may be room to speed this up, it is quite slow and takes like 2us - auto computeZeroMask = [&](uint32_t bitIndex) -> uint32_t { - const uint32_t byteIndex = bitIndex >> 3; - const uint8_t bitMask = 0x80u >> (bitIndex & 7u); - uint32_t zm = 0; - for (uint8_t ch = 0; ch < nCh; ch++) { - if (byteIndex >= chanTotalBytes[ch] || !(_BBs->pixelData[ch][byteIndex] & bitMask)) { - zm |= chanPinMask[ch]; - } - } - return zm; - }; - - // Period reference: initialise as already expired so the first pulse fires immediately. - uint32_t cyclesStart = ESP.getCycleCount() - period; - - os_intr_lock(); - - for (uint32_t bitIndex = 0; bitIndex < totalBits; bitIndex++) { - uint32_t zeroMask = computeZeroMask(bitIndex); // Compute zero mask for this bit - while ((ESP.getCycleCount() - cyclesStart) < period); // Wait for the full bit period since the last HIGH edge - - GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, setAllMask); //Set all outputs HIGH simultaneously - cyclesStart = ESP.getCycleCount(); - - while ((ESP.getCycleCount() - cyclesStart) < t0h); // After T0H — pull '0' outputs LOW - GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, zeroMask); - - while ((ESP.getCycleCount() - cyclesStart) < t1h); // After T1H — pull all remaining outputs LOW - GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, setAllMask); - } - - os_intr_unlock(); - return true; -} //============================================================================== diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index 390ccfc556..cfecb36a24 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -121,58 +121,7 @@ class Esp8266DmaBus : public PixelBus { } }; -//============================================================================== -// ESP8266 BitBang Bus (supports parallel output across multiple pins) -//============================================================================== - -class Esp8266BitBangBus : public PixelBus { -public: - Esp8266BitBangBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0); - ~Esp8266BitBangBus() override; - - bool begin() override; - void end() override; - - // Stage this channel's data. When all channels have staged, output all in parallel. - bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) override; - // BitBang output is synchronous — always ready once initialized. - bool canShow() const override { return _initialized; } -#ifdef WLED_DEBUG_BUS - const char* getTypeStr() const override { return "ESP8266_BB"; } -#endif - - void setTiming(const LedTiming& timing); - void setColorOrder(uint8_t co); - - // Reset the shared static channel registry (called when all buses are destroyed). - static void resetChannels(); - -private: - int8_t _pin; - LedTiming _timing; - bool _initialized; - // ----------------------------------------------------------------------- - // Shared state (one context for ALL Esp8266BitBangBus instances) - // All timing fields must use the same LED type (enforced by PixelBusAllocator). - // ----------------------------------------------------------------------- - struct BBstate { - uint8_t* pixelData[WLED_MAX_BB_CHANNELS]; // 4 bytes each - uint32_t allMask; // GPIO bitmask of all registered output pins - uint32_t t0h; // Timing in CPU cycles — identical across all channels - uint32_t t1h; - uint32_t period; - uint16_t numPixels[WLED_MAX_BB_CHANNELS]; - int8_t pins[WLED_MAX_BB_CHANNELS]; - uint8_t channelCount; - uint8_t stagedCount; // how many channels have called show() this frame - uint8_t pixelBytes; // bytes per encoded pixel - }; - static BBstate* _BBs; - - // Core output routine — must run from IRAM for timing accuracy - static bool IRAM_ATTR outputParallel(); -}; } // namespace WLEDpixelBus From 7e457c34bedb7e48b5831d911746676df7dab669 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 27 Jun 2026 15:01:38 +0200 Subject: [PATCH 162/173] remove debug output --- wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp index b680799be8..8a831117b8 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_BitBang.cpp @@ -254,13 +254,12 @@ void BitBangBus::resetChannels() { // 6. After T1H cycles: pull all remaining outputs LOW (GPIO.out_w1tc = setOutputMask). // // All channels are pulsed for maxPixels × pixelBytes × 8 bits. Channels that -// have fewer pixels than maxPixels simply output '0' bits once their data ends. +// have fewer pixels than maxPixels simply output '0' bits once their data ends +// Note: checked for speed, this is pretty much optimal, no need to use O2 or optimize further // --------------------------------------------------------------------------- bool IRAM_ATTR BitBangBus::outputParallel() { if (!_BBs || _BBs->channelCount == 0) return true; - uint32_t starttime = micros(); - const uint32_t t0h = _BBs->t0h; const uint32_t t1h = _BBs->t1h; const uint32_t period = _BBs->period; @@ -392,9 +391,6 @@ bool IRAM_ATTR BitBangBus::outputParallel() { #elif defined(ESP8266) os_intr_unlock(); #endif - - uint32_t endtime = micros(); - Serial.printf("took %d us\n", endtime - starttime); return true; } From 23dbb18cf96a1e8aaacbe9853b793fe339d10633 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 28 Jun 2026 10:16:11 +0200 Subject: [PATCH 163/173] remove timing update (not used), make static RMT arrays match hardware --- .../src/WLEDpixelBus/WLEDpixelBus_ESP8266.h | 4 - wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 1 - wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 1 - .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 1 - wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 77 +++---------------- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 18 +++-- wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h | 2 - 7 files changed, 22 insertions(+), 82 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h index cfecb36a24..62aac77ded 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ESP8266.h @@ -34,8 +34,6 @@ class Esp8266UartBus : public PixelBus { #ifdef WLED_DEBUG_BUS const char* getTypeStr() const override { return "ESP8266_UART"; } #endif - - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(uint8_t co); static void UartIsr(void* arg, void* exceptionFrame); @@ -86,8 +84,6 @@ class Esp8266DmaBus : public PixelBus { bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; void updateSuffix(const uint8_t* data, uint8_t len) override; void scaleAll(uint8_t scale) override; - - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(uint8_t co); static Esp8266DmaBus* s_this; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index a04e765292..fb7b9fe9ca 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -167,7 +167,6 @@ class I2sBus : public PixelBus { bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; void setInverted(bool inv) override; - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(uint8_t co); private: diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index 7d8a5e1bd0..4111c451f5 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -158,7 +158,6 @@ class LcdBus : public PixelBus { #endif void setInverted(bool inv) override; - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(uint8_t co); private: diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index 59d80893ef..e4babdefe4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -120,7 +120,6 @@ class ParallelSpiBus : public PixelBus { bool allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) override; void setInverted(bool inv) override; - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(uint8_t co); private: diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 0c2c1a67eb..d72404624a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -23,7 +23,7 @@ namespace WLEDpixelBus { //============================================================================== // Per-channel context table - stored in DRAM for ISR access -DRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[8] = {}; +DRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[WPB_RMT_CHANNELS] = {}; // Explicit IRAM tranlator callback wrappers for each channel (ensures the function is placed in IRAM which is dropped when using templates) void IRAM_ATTR RmtBus::translator_ch0(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(0, s, d, ss, w, ts, in); } @@ -39,7 +39,7 @@ void IRAM_ATTR RmtBus::translator_ch6(const void* s, rmt_item32_t* d, size_t ss, void IRAM_ATTR RmtBus::translator_ch7(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(7, s, d, ss, w, ts, in); } #endif // Jump table stored in DRAM so ISR code can quickly find the correct wrapper -DRAM_ATTR const sample_to_rmt_t RmtBus::s_callbacks[8] = { +DRAM_ATTR const sample_to_rmt_t RmtBus::s_callbacks[WPB_RMT_CHANNELS] = { RmtBus::translator_ch0, RmtBus::translator_ch1 #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 ,RmtBus::translator_ch2, RmtBus::translator_ch3 @@ -65,9 +65,6 @@ RmtBus::RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t , _initialized(false) , _usingRmtHi(false) , _rmtChannel(RMT_CHANNEL_0) - , _rmtBit0(0) - , _rmtBit1(0) - , _rmtResetTicks(0) { _encoder = ColorEncoder(colorOrder, numChannels, ledType); _ledType = ledType; @@ -86,50 +83,16 @@ void RmtBus::updateRmtTiming() { return ticks > 0 ? ticks : 1; }; - uint16_t t0h = nsToTicks(_timing.t0h_ns); - uint16_t t0l = nsToTicks(_timing.t0l_ns); - uint16_t t1h = nsToTicks(_timing.t1h_ns); - uint16_t t1l = nsToTicks(_timing.t1l_ns); - - //DEBUG_PRINTF_P(PSTR("[WPB] RMT timing (ns): t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n"), _timing.t0h_ns, _timing.t0l_ns, _timing.t1h_ns, _timing.t1l_ns, _timing.reset_us); - rmt_item32_t bit0, bit1; + bit0.level0 = 1; bit0.duration0 = nsToTicks(_timing.t0h_ns); + bit0.level1 = 0; bit0.duration1 = nsToTicks(_timing.t0l_ns); + bit1.level0 = 1; bit1.duration0 = nsToTicks(_timing.t1h_ns); + bit1.level1 = 0; bit1.duration1 = nsToTicks(_timing.t1l_ns); + + s_contexts[(int)_rmtChannel].bit0 = bit0.val; + s_contexts[(int)_rmtChannel].bit1 = bit1.val; + s_contexts[(int)_rmtChannel].resetDuration = nsToTicks(_timing.reset_us * 1000); - bit0.level0 = 1; bit0.duration0 = t0h; - bit0.level1 = 0; bit0.duration1 = t0l; - bit1.level0 = 1; bit1.duration0 = t1h; - bit1.level1 = 0; bit1.duration1 = t1l; - - _rmtBit0 = bit0.val; - _rmtBit1 = bit1.val; - _rmtResetTicks = nsToTicks(_timing.reset_us * 1000); - - // If already initialized, update hardware state for this channel - if (_initialized) { - // Update shared DRAM context used by the IDF translator (not needed for rmtHi) - if (!_usingRmtHi) { - s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; - s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; - s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; - } - // If using rmtHi, reinstall the driver with new timing templates - if (_usingRmtHi) { - // wait for any in-flight transfer, then reinstall the hi driver - RmtHiDriver::WaitForTxDone(_rmtChannel, 1000 / portTICK_PERIOD_MS); - RmtHiDriver::Uninstall(_rmtChannel); - esp_err_t instErr = RmtHiDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); - if (instErr != ESP_OK) { - //DEBUG_PRINTF_P(PSTR("[WPB] rmtHi reinstall failed: %d, falling back to IDF driver\n"), instErr); - // Try to fall back to IDF driver - if (rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)) == ESP_OK) { - rmt_translator_init(_rmtChannel, s_callbacks[(int)_rmtChannel]); - _usingRmtHi = false; - } - } else { - _usingRmtHi = true; - } - } - } } bool RmtBus::begin() { @@ -188,11 +151,6 @@ bool RmtBus::begin() { updateRmtTiming(); - // Store initial timing into the shared DRAM context for this channel - s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; - s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; - s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; - rmt_config_t config = {}; config.rmt_mode = RMT_MODE_TX; @@ -229,7 +187,7 @@ bool RmtBus::begin() { // on C3 builds, but it can be enabled explicitly with -DWLEDPB_ENABLE_RMT_HI. _usingRmtHi = false; #if !defined(CONFIG_IDF_TARGET_ESP32C3) || defined(WLEDPB_ENABLE_RMT_HI) - esp_err_t hiErr = RmtHiDriver::Install(_rmtChannel, _rmtBit0, _rmtBit1, _rmtResetTicks); + esp_err_t hiErr = RmtHiDriver::Install(_rmtChannel, s_contexts[(int)_rmtChannel].bit0 , s_contexts[(int)_rmtChannel].bit1, s_contexts[(int)_rmtChannel].resetDuration); if (hiErr == ESP_OK) { _usingRmtHi = true; } else { @@ -297,11 +255,6 @@ bool RmtBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctP // Wait for previous transmission on THIS channel to complete (IDF driver) rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); - // Update per-channel context for translator (ensure latest timings are visible to ISR) - s_contexts[(int)_rmtChannel].bit0 = _rmtBit0; - s_contexts[(int)_rmtChannel].bit1 = _rmtBit1; - s_contexts[(int)_rmtChannel].resetDuration = _rmtResetTicks; - esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); return err == ESP_OK; @@ -313,14 +266,6 @@ bool RmtBus::canShow() const { return (ESP_OK == rmt_wait_tx_done(_rmtChannel, 0)); } -void RmtBus::setTiming(const LedTiming& timing) { - //DEBUG_PRINTF_P(PSTR("[WPB] RMT setTiming called: t0h=%u t0l=%u t1h=%u t1l=%u reset_us=%u\n"), timing.t0h_ns, timing.t0l_ns, timing.t1h_ns, timing.t1l_ns, timing.reset_us); - _timing = timing; - if (_initialized) { - updateRmtTiming(); - } -} - void RmtBus::setColorOrder(uint8_t co) { _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index efe03c9639..3657b66437 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -14,6 +14,14 @@ The glitch-free high priority interrupt implementation by @willmmiles is not ava #pragma once +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 +# define WPB_RMT_CHANNELS 8 +#elif SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 +# define WPB_RMT_CHANNELS 4 +#else +# define WPB_RMT_CHANNELS 2 +#endif + #include "WLEDpixelBus.h" #include "driver/rmt.h" #include "RmtHIDriver.h" @@ -52,7 +60,6 @@ class RmtBus : public PixelBus { void setInverted(bool inv) override { _inverted = inv; } - void setTiming(const LedTiming& timing); void setColorOrder(uint8_t co); // Reset the auto-allocation counter (call before re-creating buses) @@ -70,9 +77,6 @@ class RmtBus : public PixelBus { bool _initialized; LedTiming _timing; rmt_channel_t _rmtChannel; - uint32_t _rmtBit0; - uint32_t _rmtBit1; - uint16_t _rmtResetTicks; bool _usingRmtHi; // _encodeBuffer and _encodeBufferSize are in PixelBus base @@ -92,8 +96,8 @@ class RmtBus : public PixelBus { uint16_t resetDuration; }; - // Static lookup table for ISR speed (max 8 channels) - static RmtContext s_contexts[8]; + // Static lookup table for timing speeds + static RmtContext s_contexts[WPB_RMT_CHANNELS]; // Explicit wrappers: implemented in .cpp file to ensure they are placed in IRAM static void IRAM_ATTR translator_ch0(const void* src, rmt_item32_t* dest, size_t s, size_t w, size_t* ts, size_t* in); @@ -114,7 +118,7 @@ class RmtBus : public PixelBus { size_t* translated_size, size_t* item_num); // Jump table of callbacks (defined in .cpp). Use 8 entries to match max RMT channels. - static const sample_to_rmt_t s_callbacks[8]; // TODO: could define this above and actually use the correct amount of callbacks + static const sample_to_rmt_t s_callbacks[WPB_RMT_CHANNELS]; }; } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h index aeb31abef4..4fbd415996 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_SPI.h @@ -42,8 +42,6 @@ class SpiBus : public PixelBus { #ifdef WLED_DEBUG_BUS const char* getTypeStr() const override { return _useHardware ? "HW_SPI" : "SW_SPI"; } #endif - - void setTiming(const LedTiming& timing) { _timing = timing; } void setColorOrder(uint8_t co); /** From 963708b47f272368c5b7a90e784468ac9b449b22 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 28 Jun 2026 12:29:50 +0200 Subject: [PATCH 164/173] add multi-mem block support to RMTHI driver, clean up code, some optimization --- wled00/bus_manager.cpp | 1 - wled00/bus_wrapper.h | 25 ++------- wled00/src/WLEDpixelBus/RmtHIDriver.h | 2 +- wled00/src/WLEDpixelBus/RmtHiDriver.cpp | 32 ++++++++---- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 4 +- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 3 +- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 54 ++++++++++---------- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 4 +- 8 files changed, 56 insertions(+), 69 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 2200134782..4d3c65c1dc 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -1579,7 +1579,6 @@ ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } // PixelBusAllocator channel tracking for dynamic allocation #ifndef ESP8266 uint8_t PixelBusAllocator::_rmtChannelsAssigned = 0; // number of RMT channels assigned during allocateHardware() -uint8_t PixelBusAllocator::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() uint8_t PixelBusAllocator::_i2sChannelsAssigned = 0; uint8_t PixelBusAllocator::_parallelI2sBusType = 0; uint8_t PixelBusAllocator::_bitBangChannelsAssigned = 0; diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index f73f78200e..e1fd2643c5 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -20,16 +20,13 @@ #define P_8266_HS_CLK 14 // Use single RMT memory block per channel — allows RMT RX channels alongside TX. -// TODO: hi priority RMT driver does not yet support multi-memory block -#ifndef CONFIG_IDF_TARGET_ESP32C3 - #define RMT_USE_SINGLE_MEM_BLOCK -#endif +//#define RMT_USE_SINGLE_MEM_BLOCK + class PixelBusAllocator { private: #ifndef ESP8266 static uint8_t _rmtChannelsAssigned; - static uint8_t _rmtChannel; static uint8_t _i2sChannelsAssigned; static uint8_t _parallelI2sBusType; // Track first I2S type to enforce parallel timing static uint8_t _bitBangChannelsAssigned; @@ -43,7 +40,6 @@ class PixelBusAllocator { static void resetChannelTracking() { #ifndef ESP8266 _rmtChannelsAssigned = 0; - _rmtChannel = 0; _i2sChannelsAssigned = 0; _parallelI2sBusType = 0; // TYPE_NONE _bitBangChannelsAssigned = 0; @@ -177,23 +173,8 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l } #endif - int8_t rmtCh = -1; // -1 = auto-select; file-scope RMT_USE_SINGLE_MEM_BLOCK switches to sequential assignment - #ifndef ESP8266 - if (driver == WLEDpixelBus::BusDriver::RMT) { - if (_rmtChannel < WLED_MAX_RMT_CHANNELS) { - #ifdef RMT_USE_SINGLE_MEM_BLOCK - rmtCh = _rmtChannel++; // assign channels in order, do not use auto-channel function (this uses 1 memory block per channel allowing RX RMT channels to be used as well) - #else - _rmtChannel++; // increment channel count for tracking, but use auto-channel to optimize memory block allocation - #endif - } else { - return nullptr; - } - } - #endif - // Chip-specific init (prefix/suffix/invert) is applied inside createBus() using ledType. - return WLEDpixelBus::createBus(driver, pins[0], timing, colorOrder, numChannels, WLEDpixelBus::DEFAULT_DMA_BUFFER_SIZE, rmtCh, busType); + return WLEDpixelBus::createBus(driver, pins[0], timing, colorOrder, numChannels, busType); } }; #endif diff --git a/wled00/src/WLEDpixelBus/RmtHIDriver.h b/wled00/src/WLEDpixelBus/RmtHIDriver.h index f2f8028881..06fee873d0 100644 --- a/wled00/src/WLEDpixelBus/RmtHIDriver.h +++ b/wled00/src/WLEDpixelBus/RmtHIDriver.h @@ -14,7 +14,7 @@ by @willmmiles, 2026 namespace RmtHiDriver { // Install the driver for a specific channel, specifying timing properties - esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration); + esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset, uint8_t blocksToUse = 1); // Remove the driver on a specific channel esp_err_t Uninstall(rmt_channel_t channel); diff --git a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp index 3be9ae34ce..69f876f3c8 100644 --- a/wled00/src/WLEDpixelBus/RmtHiDriver.cpp +++ b/wled00/src/WLEDpixelBus/RmtHiDriver.cpp @@ -135,13 +135,19 @@ static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t chan dev->int_clr.val = (1 << (channel + 24)); } - __attribute__((always_inline)) static inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev) { uint32_t status = dev->int_st.val; return (status & 0xFF000000) >> 24; } + +__attribute__((always_inline)) +static inline void rmt_ll_tx_set_mem_blocks(rmt_dev_t *dev, uint32_t channel, uint8_t block_num) +{ + dev->conf_ch[channel].conf0.mem_size = block_num; +} + #endif @@ -207,8 +213,9 @@ struct NeoEsp32RmtHIChannelState { const byte* txDataStart; // data array const byte* txDataEnd; // one past end - const byte* txDataCurrent; // current location + const byte* txDataCurrent; // current location size_t rmtOffset; + size_t batchSize; // memory blocks assigned }; // Global variables @@ -217,18 +224,17 @@ 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) { +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 = rmtBatchSize - reserve; + size_t rmtToWrite = batchSize - reserve; rmt_item32_t* dest =(rmt_item32_t*) &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory const byte* psrc = *src_ptr; - *offset_ptr ^= rmtBatchSize; + *offset_ptr ^= batchSize; if (psrc != end) { while (rmtToWrite > 0) { @@ -271,8 +277,8 @@ static void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& 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); + 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); @@ -288,7 +294,7 @@ extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { if (driverState[channel]) { // Normal case NeoEsp32RmtHIChannelState& state = *driverState[channel]; - RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); + 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); @@ -334,7 +340,7 @@ static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t stat } -esp_err_t RmtHiDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset) { +esp_err_t RmtHiDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset, uint8_t blocksToUse) { // Validate channel number if (channel >= RMT_CHANNEL_MAX) { return ESP_ERR_INVALID_ARG; @@ -390,9 +396,13 @@ esp_err_t RmtHiDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t state->rmtBit1 = rmtBit1; state->resetDuration = reset; + // Calculate batch size based on number of blocks (64 items per block) + state->batchSize = (RMT_MEM_ITEM_NUM * blocksToUse) / 2; + // Initialize hardware rmt_ll_tx_stop(&RMT, channel); rmt_ll_tx_reset_pointer(&RMT, channel); + rmt_ll_tx_set_mem_blocks(&RMT, channel, blocksToUse); rmt_ll_enable_tx_err_interrupt(&RMT, channel, false); rmt_ll_enable_tx_end_interrupt(&RMT, channel, false); rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); @@ -401,7 +411,7 @@ esp_err_t RmtHiDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t 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); + rmt_ll_tx_set_limit(&RMT, channel, state->batchSize); driverState[channel] = state; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index a6297c8d40..df29f31a4f 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -207,14 +207,14 @@ uint32_t ColorEncoder::decodeGeneric(const uint8_t* in) const { // Bus Factory Implementation //============================================================================== -PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, size_t bufferSize, int8_t channel, uint8_t ledType) { +PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType, size_t bufferSize) { PixelBus* bus = nullptr; switch (driver) { #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) case BusDriver::RMT: - bus = new RmtBus(pin, timing, colorOrder, numChannels, channel, ledType); + bus = new RmtBus(pin, timing, colorOrder, numChannels, ledType); break; #ifdef WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 0416a26e51..5405b6232a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -560,8 +560,7 @@ constexpr uint8_t getRmtMaxChannels() { * @return Bus instance (caller owns, delete when done) */ PixelBus* createBus(BusDriver type, int8_t pin, const LedTiming& timing, - uint8_t colorOrder, uint8_t numChannels, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, - int8_t channel = -1, uint8_t ledType = 0); + uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE); } // namespace WLEDpixelBus diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index d72404624a..f643eb5049 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -57,9 +57,8 @@ uint8_t RmtBus::s_currentChannelIndex = 0; uint8_t RmtBus::s_usedBlocks = 0; uint8_t RmtBus::s_activeChannelMask = 0; -RmtBus::RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, int8_t channel, uint8_t ledType) +RmtBus::RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) : _pin(pin) - , _channel(channel) , _timing(timing) , _inverted(false) , _initialized(false) @@ -107,40 +106,40 @@ bool RmtBus::begin() { // C3 and S3 have less channels than blocks, so the last channel can always use additional blocks // example: ESP32, 2 channels requested total -> use CH0 with 4 blocks and CH4 with 4 blocks // example: ESP32-S3 with 3 channels: use CH0 with 2 block, CH2 with 1 block, and CH3 with 5 blocks - if (_channel < 0) { - if (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) { - return false; - } + if (s_allocatedCount >= s_expectedChannels || s_allocatedCount >= maxTxChannels) + return false; - uint8_t totalBlocks; + uint8_t totalBlocks; #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) - totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory + totalBlocks = 8; // ESP32 and S3 have 8 blocks of RMT memory #elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) // note: C6 RMT hardware is the same as C3 - totalBlocks = 4; // other supported ESP32 variants have 4 blocks + totalBlocks = 4; // other supported ESP32 variants have 4 blocks #else - totalBlocks = 4; // default to 4 if unknown, should be safe + totalBlocks = 4; // default to 4 if unknown, should be safe #endif - int left_channels = s_expectedChannels - s_allocatedCount - 1; - - if (left_channels == 0) { - _channel = s_currentChannelIndex; - blocksToUse = totalBlocks - s_usedBlocks; - } else { - int k = totalBlocks / s_expectedChannels; - int max_k_for_index = maxTxChannels - s_currentChannelIndex - left_channels; - if (k > max_k_for_index) k = max_k_for_index; - if (k < 1) k = 1; + int left_channels = s_expectedChannels - s_allocatedCount - 1; - _channel = s_currentChannelIndex; - blocksToUse = k; - } + if (left_channels == 0) { + _channel = s_currentChannelIndex; + blocksToUse = totalBlocks - s_usedBlocks; + } else { + int k = totalBlocks / s_expectedChannels; + int max_k_for_index = maxTxChannels - s_currentChannelIndex - left_channels; + if (k > max_k_for_index) k = max_k_for_index; + if (k < 1) k = 1; - s_currentChannelIndex += blocksToUse; - s_usedBlocks += blocksToUse; - s_allocatedCount++; + _channel = s_currentChannelIndex; + blocksToUse = k; } + s_currentChannelIndex += blocksToUse; + s_usedBlocks += blocksToUse; + s_allocatedCount++; +#ifdef RMT_USE_SINGLE_MEM_BLOCK + blocksToUse = 1; +#endif + if (_channel >= (int8_t)maxTxChannels) { //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d >= max %u, FAIL\n"), _channel, maxTxChannels); return false; @@ -170,6 +169,7 @@ bool RmtBus::begin() { } // Register hack for memory blocks normally assigned to RX (S2 / S3 / C3) TODO: need this for ESP32 as well? + // set owner to TX if blocks are shared with RX unit #ifndef RMT_USE_SINGLE_MEM_BLOCK #if defined(CONFIG_IDF_TARGET_ESP32S3) for (int i = 4; i < 8; i++) { @@ -187,7 +187,7 @@ bool RmtBus::begin() { // on C3 builds, but it can be enabled explicitly with -DWLEDPB_ENABLE_RMT_HI. _usingRmtHi = false; #if !defined(CONFIG_IDF_TARGET_ESP32C3) || defined(WLEDPB_ENABLE_RMT_HI) - esp_err_t hiErr = RmtHiDriver::Install(_rmtChannel, s_contexts[(int)_rmtChannel].bit0 , s_contexts[(int)_rmtChannel].bit1, s_contexts[(int)_rmtChannel].resetDuration); + esp_err_t hiErr = RmtHiDriver::Install(_rmtChannel, s_contexts[(int)_rmtChannel].bit0 , s_contexts[(int)_rmtChannel].bit1, s_contexts[(int)_rmtChannel].resetDuration, blocksToUse); if (hiErr == ESP_OK) { _usingRmtHi = true; } else { diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 3657b66437..3842f2f28d 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -40,10 +40,8 @@ class RmtBus : public PixelBus { * @param pin GPIO pin * @param timing LED timing * @param order Color order - * @param channel RMT channel (-1 for auto) */ - RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, - int8_t channel = -1, uint8_t ledType = 0); + RmtBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType = 0); ~RmtBus() override; bool begin() override; From 6ac48fe46b33f897abaec2dd43f763cf7167f077 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 29 Jun 2026 20:56:21 +0200 Subject: [PATCH 165/173] Improve I2S driver - new state machine that resprects proper reset time, improves frame rate - use 3 buffers on ESP32 and S3 as RMT can cause glitches, also increase buffer size if necessary - calculate buffer size from LEDs used, allocate smaller buffer if large buffer is not needed --- wled00/FX.h | 2 - wled00/bus_wrapper.h | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 13 +- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 10 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 226 ++++++++++--------- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 41 ++-- 6 files changed, 164 insertions(+), 130 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index d001d2cbc6..5030aa080b 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -96,8 +96,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex() assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) -#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) - #define NUM_COLORS 3 /* number of colors per segment */ #define SEGMENT (*strip._currentSegment) #define SEGENV (*strip._currentSegment) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index e1fd2643c5..072a690e7a 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -174,7 +174,7 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l #endif // Chip-specific init (prefix/suffix/invert) is applied inside createBus() using ledType. - return WLEDpixelBus::createBus(driver, pins[0], timing, colorOrder, numChannels, busType); + return WLEDpixelBus::createBus(driver, pins[0], timing, colorOrder, numChannels, busType, len); } }; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index df29f31a4f..a0ae8f88a2 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -207,7 +207,7 @@ uint32_t ColorEncoder::decodeGeneric(const uint8_t* in) const { // Bus Factory Implementation //============================================================================== -PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType, size_t bufferSize) { +PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType, size_t numPixels) { PixelBus* bus = nullptr; @@ -218,11 +218,14 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 break; #ifdef WLEDPB_I2S_SUPPORT - case BusDriver::I2S: -#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - bus = new I2sBus(pin, timing, colorOrder, numChannels, 0, bufferSize, ledType); + case BusDriver::I2S: +#if defined(CONFIG_IDF_TARGET_ESP32C3) + #error C3 hardware does not support parallel I2S output and single channel output is not implemented, use WLEDPB_PARALLEL_SPI_SUPPORT instead +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S2) + bus = new I2sBus(pin, timing, colorOrder, numChannels, 0, ledType, numPixels); #else - bus = new I2sBus(pin, timing, colorOrder, numChannels, 1, bufferSize, ledType); + bus = new I2sBus(pin, timing, colorOrder, numChannels, 1, ledType, numPixels); #endif break; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index 5405b6232a..c1cebd1f28 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -142,10 +142,14 @@ enum class DriverState : uint8_t { // DMA Buffer Configuration //============================================================================== - -constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*3); constexpr size_t MIN_DMA_BUFFER_SIZE = 256; constexpr size_t MAX_DMA_BUFFER_SIZE = 4092; +#if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT +constexpr size_t DEFAULT_DMA_BUFFER_SIZE = MAX_DMA_BUFFER_SIZE; // note: 3k is enough except if using 8RMT, need extra space for S3 and ESP32 classic, also requires triple buffering +#else +constexpr size_t DEFAULT_DMA_BUFFER_SIZE = (1024*3); +#endif +// TODO: need to assert the default is not larger than the max allowed, which is 4092 (12bit in DMA descriptor) //============================================================================== // Color Encoding Helpers @@ -554,9 +558,9 @@ constexpr uint8_t getRmtMaxChannels() { * @param timing LED timing * @param colorOrder Color order byte * @param numChannels Bytes per pixel in the encoded stream - * @param bufferSize DMA buffer size (for I2S/LCD) * @param channel RMT channel to use (-1 for auto-allocate) * @param ledType WLED LED type constant (TYPE_*), used for chip-specific init + * @param bufferSize DMA buffer size (for I2S/LCD) * @return Bus instance (caller owns, delete when done) */ PixelBus* createBus(BusDriver type, int8_t pin, const LedTiming& timing, diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index c1cff9d0a2..bf05724144 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -57,9 +57,12 @@ I2sBusContext::I2sBusContext(uint8_t busNum) : _busNum(busNum) , _state(DriverState::Idle) , _initialized(false) + , _maxSrcBytes(0) , _bufferSize(0) + , _dmaAllocated(false) , _activeBuffer(0) , _remainingDataBuffers(0) + , _resetBytesLeft(0) , _timing{0, 0, 0, 0, 0} , _clockDiv(1) , _isrHandle(nullptr) @@ -88,42 +91,14 @@ I2sBusContext:: ~I2sBusContext() { deinit(); } -bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { +bool I2sBusContext::init(const LedTiming& timing) { if (_initialized) return true; //pinMode(33, OUTPUT); // debug pin for timing analysis _timing = timing; - _bufferSize = (bufferSize + 3) & ~3; // align to 4 bytes - - // Allocate DMA buffers (4-byte aligned for DMA) - for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - //Serial.println("I2S DMA buffer alloc failed"); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, _bufferSize); - - _dmaDesc[i] = (lldesc_t*)heap_caps_aligned_alloc(4, sizeof(lldesc_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - //Serial.println("I2S DMA desc alloc failed"); - deinit(); - return false; - } - } - - // Setup DMA descriptors - circular chain - for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i]->size = _bufferSize; - _dmaDesc[i]->length = _bufferSize; - _dmaDesc[i]->buf = _dmaBuffer[i]; - _dmaDesc[i]->eof = 1; // Generate interrupt on completion - _dmaDesc[i]->sosf = 0; - _dmaDesc[i]->owner = 1; - _dmaDesc[i]->qe.stqe_next = _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]; - } + // DMA buffer allocation is deferred to _allocDmaBuffers(), called from startTransmit() once + // all channels have registered and _maxSrcBytes reflects the largest bus. // Enable I2S peripheral #if defined(CONFIG_IDF_TARGET_ESP32) @@ -304,7 +279,7 @@ bool I2sBusContext::init(const LedTiming& timing, size_t bufferSize) { 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); + 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) { //DEBUG_PRINTF("I2S ISR alloc failed: %d", err); deinit(); @@ -349,10 +324,48 @@ void I2sBusContext::deinit() { periph_module_disable(PERIPH_I2S0_MODULE); #endif + _dmaAllocated = false; _initialized = false; } -int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, bool inverted) { +// Compute the DMA buffer size from _maxSrcBytes (set during registerChannel() by the largest bus), +// then allocate the circular DMA descriptor/buffer chain. +// Called once from startTransmit() after all channels have registered. +bool I2sBusContext::_allocDmaBuffers() { + // DMA buffer size: spread total pixel data evenly across the circular buffer slots, + // then clamp to [MIN_DMA_BUFFER_SIZE, DEFAULT_DMA_BUFFER_SIZE] + _bufferSize = (WLEDPB_I2S_DMABYTES * _maxSrcBytes) / WLEDPB_I2S_DMA_BUFFER_COUNT; + _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes + if (_bufferSize > DEFAULT_DMA_BUFFER_SIZE) _bufferSize = DEFAULT_DMA_BUFFER_SIZE; + if (_bufferSize < MIN_DMA_BUFFER_SIZE) _bufferSize = MIN_DMA_BUFFER_SIZE; + + // allocate DMA-capable buffers (4-byte aligned for hardware DMA engine) + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) return false; + memset(_dmaBuffer[i], 0, _bufferSize); + + _dmaDesc[i] = (lldesc_t*)heap_caps_aligned_alloc(4, sizeof(lldesc_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) return false; + } + + // set up DMA descriptors as a circular linked list + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + _dmaDesc[i]->size = _bufferSize; + _dmaDesc[i]->length = _bufferSize; + _dmaDesc[i]->buf = _dmaBuffer[i]; + _dmaDesc[i]->eof = 1; // generate EOF interrupt on completion of each buffer + _dmaDesc[i]->sosf = 0; + _dmaDesc[i]->owner = 1; // hand ownership to DMA engine + _dmaDesc[i]->qe.stqe_next = _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]; + } + + _dmaAllocated = true; + //DEBUG_PRINTF_P(PSTR("[I2S] DMA buffers allocated: bufSize=%u x%u\n"), _bufferSize, WLEDPB_I2S_DMA_BUFFER_COUNT); + return true; +} + +int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, size_t srcBytes, bool inverted) { // Find free slot int8_t idx = -1; for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { @@ -370,6 +383,9 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, bool inverted) { _channelCount++; _channelMask |= (1 << idx); + // track the largest source byte count across channels; used in _allocDmaBuffers() to size DMA buffers + if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; + // Configure GPIO gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); @@ -437,19 +453,15 @@ void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ #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 maxCh = 0; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; - } - +// Returns the number of bytes actually written into dest (may be < destLen when data runs out). +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { for (size_t pos = 0; pos + 64 <= destLen; pos += 64) { // alwaysMask: channels with active data (HIGH step); bN: channels with bit N set uint16_t alwaysMask = 0; uint16_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; // named regs: compiler should keep in regs uint16_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; - for (int ch = 0; ch < maxCh; ch++) { + for (int ch = 0; ch < maxChannel; ch++) { if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; const uint16_t m = (uint16_t)(1u << ch); @@ -492,21 +504,15 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { } } -#else // WLED_PIXELBUS_16PARALLEL - +#else // 8-bit parallel encode: branchless gather + scatter, 32 bytes per source byte (8 channels) -void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; - } - +void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { uint8_t alwaysMask = 0; uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; - for (int ch = 0; ch < maxCh; ch++) { + for (int ch = 0; ch < maxChannel; ch++) { if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; const uint8_t m = (uint8_t)(1u << ch); @@ -554,9 +560,48 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen) { #endif // WLED_PIXELBUS_16PARALLEL -void I2sBusContext::fillBuffer(uint8_t bufIdx) { - encode4Step(_dmaBuffer[bufIdx], _bufferSize); - // desc->length stays at _bufferSize (set in init, never changes) +void IRAM_ATTR I2sBusContext::fillBuffer(uint8_t bufIdx) { + // clear the buffer + memset(_dmaBuffer[bufIdx], 0, _bufferSize); // clear the buffer, will be filled or left blank as reset signal (cant be skipped, some channels may have less data) + + if (_resetBytesLeft > 0) { + _dmaDesc[bufIdx]->length = _resetBytesLeft; + _dmaDesc[bufIdx]->qe.stqe_next = nullptr; // stop the engine after this one note: this assumes the reset period fits within one buffer, so don't set buffers too small i.e. at least 1k + _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers (is a multiple of 4 if set "naturally" below) + return; // nothing to encode, this is a reset pulse, keep output low (all zeroes) + } + uint32_t bytesToEncode = 0; + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + maxCh = ch + 1; + uint32_t channelBytesLeft = _channels[ch].srcLen - _channels[ch].srcPos; + if (channelBytesLeft > bytesToEncode) bytesToEncode = channelBytesLeft; + } + } + + uint32_t translatedbytes = bytesToEncode * WLEDPB_I2S_DMABYTES; + translatedbytes = translatedbytes > _bufferSize ? _bufferSize : translatedbytes; + encode4Step(_dmaBuffer[bufIdx], translatedbytes, maxCh); + + if (translatedbytes < _bufferSize) { + // Data ran out before the buffer was full (i.e. we are done), compute the minimum reset period we must send as zero cycles + uint32_t resetNs = _timing.reset_us * 1000; + uint32_t bitPeriodNs = _timing.bitPeriod() + 1; // +1 to ensure no division by zero and slightly over-estimate the reset cycle + uint32_t zeroCycles = resetNs / bitPeriodNs; + size_t resetBytes = zeroCycles * (WLEDPB_I2S_DMABYTES / 8); // one cycle is 4 clocks, on each clock two/one buffer byte(s) sent out in parallel + + size_t newLen = translatedbytes + resetBytes; + if (newLen > _bufferSize) { + _resetBytesLeft = newLen - _bufferSize; // reset pulse does not fit into this buffer frame, send another one (see above) + _dmaDesc[bufIdx]->length = _bufferSize; + } + else { + _dmaDesc[bufIdx]->length = newLen; // send the rest (zeroes) as a reset + _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers or it will mess up the DMA + _dmaDesc[bufIdx]->qe.stqe_next = nullptr; // reset fit into this buffer, end transfer after this is sent + } + } } bool I2sBusContext::startTransmit() { @@ -577,13 +622,24 @@ bool I2sBusContext::startTransmit() { } } + _resetBytesLeft = 0; + + // allocate or reallocate DMA buffers if needed — deferred from init() so all channels + // can register first and _maxSrcBytes reflects the bus with the most pixels + if (!_dmaAllocated) { + if (!_allocDmaBuffers()) return false; + } + // Fill all buffers initially for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + _dmaDesc[i]->qe.stqe_next = _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]; // restore circular buffer chain + _dmaDesc[i]->length = _bufferSize; fillBuffer(i); - _dmaDesc[i]->owner = 1; // Restore ownership after descriptor init + _dmaDesc[i]->eof = 1; // enable eof, just in case + _dmaDesc[i]->owner = 1; // hand ownership over to DMA after descriptor init } - _activeBuffer = 0; + _activeBuffer = 0; // start with first buffer _remainingDataBuffers = WLEDPB_I2S_DMA_BUFFER_COUNT; _state = DriverState::Sending; @@ -627,62 +683,25 @@ void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { // The completed buffer just finished playing; DMA is now on the next buffer uint8_t completedBuf = ctx->_activeBuffer; ctx->_activeBuffer = (ctx->_activeBuffer + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; - memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); // clear the buffer, will be filled or left blank as reset signal - - if (ctx->_state == DriverState::Sending) { - // Encode next chunk into the completed buffer - // encode4Step always fills the full buffer (zeros for any remainder) - ctx->encode4Step(ctx->_dmaBuffer[completedBuf], ctx->_bufferSize); - - // Check if all source data has been consumed - bool moreData = false; - for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && - ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { - moreData = true; - break; - } - } - if (!moreData) { - // Last data chunk was just encoded into completedBuf. - // DMA is currently playing the next buffer and remaining data buffers are pending. - ctx->_remainingDataBuffers = WLEDPB_I2S_DMA_BUFFER_COUNT; - ctx->_state = DriverState::SendingLast; - } - - // Restore DMA ownership so DMA can use this buffer - ctx->_dmaDesc[completedBuf]->owner = 1; - } else if (ctx->_state == DriverState::SendingLast) { - // One data buffer has just finished; convert it to reset data. - if (ctx->_remainingDataBuffers > 0) { - ctx->_remainingDataBuffers--; - } - - ctx->_dmaDesc[completedBuf]->owner = 1; - - if (ctx->_remainingDataBuffers == 0) { - // We have cycled through all pending data buffers and have zeroed the last one. - // The next buffer to play will be reset data. Wait for that one finish then stop. - ctx->_state = DriverState::WaitingReset; - } else { - ctx->_state = DriverState::SendingLast; - } - } else { - // WaitingReset - last data played, zero buffer sent as reset. Stop DMA. - dev->int_ena.out_eof = 0; + if (ctx->_dmaDesc[completedBuf]->qe.stqe_next == nullptr) { // this was the last buffer + //ctx->_state = DriverState::WaitingReset; // if resetBytesLeft is not a multiple of 4 this flags end of transfer + dev->int_ena.out_eof = 0; // disable interrupt dev->conf.tx_start = 0; dev->out_link.start = 0; ctx->_state = DriverState::Idle; + return; + } else if ((ctx->_resetBytesLeft != 3)) { // 3 means end of transfer (i.e. reset pulse) was encoded on last fillBuffer call + ctx->fillBuffer(completedBuf); // fill buffer, handle reset pulse and end of + ctx->_dmaDesc[completedBuf]->owner = 1; // owner is reset upon eof and must be handed back to DMA, do not hand it over if bus is stopped or it breaks the state-machine + return; } } // I2sBus implementation -I2sBus::I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, - uint8_t busNum, size_t bufferSize, uint8_t ledType) +I2sBus::I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum, uint8_t ledType, size_t numPixels) : _pin(pin) , _busNum(busNum) - , _bufferSize(bufferSize) , _timing(timing) , _initialized(false) , _channelIdx(-1) @@ -690,6 +709,7 @@ I2sBus::I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t { _encoder = ColorEncoder(colorOrder, numChannels, ledType); _ledType = ledType; + _numPixels = numPixels; // stored so begin() can report srcBytes to the shared I2sBusContext for DMA sizing } I2sBus::~I2sBus() { @@ -702,13 +722,15 @@ bool I2sBus::begin() { _ctx = I2sBusContext::get(_busNum); if (!_ctx) return false; - if (!_ctx->init(_timing, _bufferSize)) { + if (!_ctx->init(_timing)) { I2sBusContext::release(_busNum); _ctx = nullptr; return false; } - _channelIdx = _ctx->registerChannel(_pin, this, _inverted); + // pass our encoded byte count so the context can size DMA buffers for the largest bus + const size_t srcBytes = (size_t)_numPixels * _encoder.getPixelBytes(); + _channelIdx = _ctx->registerChannel(_pin, this, srcBytes, _inverted); if (_channelIdx < 0) { //DEBUG_PRINTF_P(PSTR("[I2S] registerChannel failed for pin %d\n"), _pin); I2sBusContext::release(_busNum); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index fb7b9fe9ca..dec495af37 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -39,11 +39,6 @@ namespace WLEDpixelBus { #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 LED, 1k buffer can hold ~5 LEDs, ISR will fire every 144us -// TODO: 16 parallel channels does not make much sense to use as a default. should enable both 8 and 16 parallel channels. -// -// on ESP32, 8*768 works with 8RMT, 8*512 does not. 6*768 also flickers, 6*1024 works -// with improved DMA ISR: works with 3x2k buffer - // I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering is required for 16-parallel output. // TODO: this requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load @@ -52,31 +47,38 @@ namespace WLEDpixelBus { #ifdef WLED_PIXELBUS_16PARALLEL #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 #else - #define WLEDPB_I2S_DMA_BUFFER_COUNT 2 + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT + #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 + #else + #define WLEDPB_I2S_DMA_BUFFER_COUNT 2 // 2 buffers is enough if not constantly interrupted by RMT + #endif #endif #endif // 16-bit parallel mode supports 16 channels; 8-bit supports 8 channels. #ifdef WLED_PIXELBUS_16PARALLEL #define WLEDPB_I2S_MAX_CHANNELS 16 + #define WLEDPB_I2S_DMABYTES 64 // 64 bytes per pixel byte (4 clocks per bit, 2 bytes per clock) #else #define WLEDPB_I2S_MAX_CHANNELS 8 + #define WLEDPB_I2S_DMABYTES 32 // 32 bytes per pixel byte (4 clocks per bit, 1 byte per clock) #endif + /** * I2S bus context - manages shared I2S peripheral for parallel output * Uses circular DMA buffers with ISR-driven buffer refill */ class I2sBusContext { -public: +public: static I2sBusContext* get(uint8_t busNum); static void release(uint8_t busNum); - bool init(const LedTiming& timing, size_t bufferSize); + bool init(const LedTiming& timing); void deinit(); // Channel management - int8_t registerChannel(int8_t pin, I2sBus* bus, bool inverted = false); + int8_t registerChannel(int8_t pin, I2sBus* bus, size_t srcBytes, bool inverted = false); void unregisterChannel(int8_t channelIdx); uint8_t getChannelCount() const { return _channelCount; } @@ -91,7 +93,7 @@ class I2sBusContext { I2sBusContext(uint8_t busNum); ~I2sBusContext(); - void fillBuffer(uint8_t bufIdx); + void IRAM_ATTR fillBuffer(uint8_t bufIdx); static void IRAM_ATTR dmaISR(void* arg); uint8_t _busNum; @@ -99,12 +101,17 @@ class I2sBusContext { volatile DriverState _state; bool _initialized; + bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel + // DMA circular buffer chain lldesc_t* _dmaDesc[WLEDPB_I2S_DMA_BUFFER_COUNT]; uint8_t* _dmaBuffer[WLEDPB_I2S_DMA_BUFFER_COUNT]; - size_t _bufferSize; + size_t _bufferSize; // actual allocated DMA buffer size (per buffer) + size_t _maxSrcBytes; // max source (encoded pixel) bytes across all registered channels; drives DMA sizing + bool _dmaAllocated; // true when DMA buffers are allocated and reflect current _maxSrcBytes volatile uint8_t _activeBuffer; volatile uint8_t _remainingDataBuffers; + volatile uint16_t _resetBytesLeft; // Timing LedTiming _timing; @@ -129,7 +136,7 @@ class I2sBusContext { size_t _maxDataLen; // Encoding (4-step cadence) - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); // Singleton instances static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; @@ -145,12 +152,13 @@ class I2sBus : public PixelBus { * Create I2S bus * @param pin GPIO pin * @param timing LED timing - * @param order Color order + * @param colorOrder Color order + * @param numChannels Bytes per pixel * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) - * @param bufferSize DMA buffer size + * @param ledType LED chip type constant + * @param numPixels Number of pixels; stored for DMA buffer sizing in I2sBusContext */ - I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, - uint8_t busNum = 1, size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, uint8_t ledType = 0); + I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum = 1, uint8_t ledType = 0, size_t numPixels = 0); ~I2sBus() override; bool begin() override; @@ -172,7 +180,6 @@ class I2sBus : public PixelBus { private: int8_t _pin; uint8_t _busNum; - size_t _bufferSize; LedTiming _timing; bool _inverted = false; bool _initialized; From 649213758da451ba95f57a8d2f3dd89c1678ddcc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 30 Jun 2026 18:07:50 +0200 Subject: [PATCH 166/173] port I2S improvements to LCD bus. should unify the two... --- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 2 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 7 +- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 288 +++++++++---------- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 26 +- 4 files changed, 151 insertions(+), 172 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index a0ae8f88a2..6698aaf87b 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -232,7 +232,7 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 #ifdef WLEDPB_LCD_SUPPORT case BusDriver::LCD: - bus = new LcdBus(pin, timing, colorOrder, numChannels, bufferSize, false, ledType); + bus = new LcdBus(pin, timing, colorOrder, numChannels, numPixels, false, ledType); break; #endif diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index dec495af37..1e12eea87b 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -94,6 +94,8 @@ class I2sBusContext { ~I2sBusContext(); void IRAM_ATTR fillBuffer(uint8_t bufIdx); + bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); // Encoding (4-step cadence) static void IRAM_ATTR dmaISR(void* arg); uint8_t _busNum; @@ -101,8 +103,6 @@ class I2sBusContext { volatile DriverState _state; bool _initialized; - bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel - // DMA circular buffer chain lldesc_t* _dmaDesc[WLEDPB_I2S_DMA_BUFFER_COUNT]; uint8_t* _dmaBuffer[WLEDPB_I2S_DMA_BUFFER_COUNT]; @@ -135,9 +135,6 @@ class I2sBusContext { uint16_t _stagedMask; size_t _maxDataLen; - // Encoding (4-step cadence) - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); - // Singleton instances static I2sBusContext* _instances[WLEDPB_I2S_BUS_COUNT]; static uint8_t _refCount[WLEDPB_I2S_BUS_COUNT]; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp index 033e54e329..432d447dd1 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp @@ -34,6 +34,8 @@ Key design: - Buffers are filled with data or zeros (for reset period) - Stop only after reset period completes on buffer boundary - No DMA reconfiguration during transmission +- DMA buffer size is computed from the largest registered bus and allocated + once in startTransmit() (deferred allocation). -------------------------------------------------------------------------*/ #include "driver/periph_ctrl.h" @@ -77,13 +79,15 @@ LcdBusContext::LcdBusContext() , _use16Bit(false) , _dmaChannel(nullptr) , _timing{0, 0, 0, 0, 0} - , _bufferSize(WLEDPB_LCD_DMA_BUFFER_SIZE) + , _bufferSize(0) , _channelCount(0) , _channelMask(0) , _stagedMask(0) , _maxDataLen(0) , _activeBuffer(0) - , _remainingDataBuffers(0) + , _maxSrcBytes(0) // HEADER + , _resetBytesLeft(0) // HEADER + , _dmaAllocated(false) // HEADER { for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; @@ -98,50 +102,17 @@ LcdBusContext::~LcdBusContext() { deinit(); } -bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16Bit) { +bool LcdBusContext::init(const LedTiming& timing, bool use16Bit) { if (_initialized) return true; - LCD_LOG("Init: buf=%u x%d, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", - WLEDPB_LCD_DMA_BUFFER_SIZE, WLEDPB_LCD_DMA_BUFFER_COUNT, WLEDPB_LCD_CADENCE_STEPS, - timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); + LCD_LOG("Init: deferred alloc, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", + WLEDPB_LCD_CADENCE_STEPS, timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); _timing = timing; _use16Bit = use16Bit; - _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; uint32_t bitPeriodNs = timing.bitPeriod(); - // Allocate DMA buffers - for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_LCD_DMA_BUFFER_SIZE, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - LCD_LOG("ERROR: DMA buffer %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, WLEDPB_LCD_DMA_BUFFER_SIZE); - - _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - LCD_LOG("ERROR: DMA desc %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); - } - - // Setup DMA descriptors in CIRCULAR mode (never changes during operation!) - for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i]->dw0.size = WLEDPB_LCD_DMA_BUFFER_SIZE; - _dmaDesc[i]->dw0.length = WLEDPB_LCD_DMA_BUFFER_SIZE; - _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR - _dmaDesc[i]->buffer = _dmaBuffer[i]; - _dmaDesc[i]->next = _dmaDesc[(i + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT]; // circular chain - } - - - // Enable LCD_CAM peripheral periph_module_enable(PERIPH_LCD_CAM_MODULE); periph_module_reset(PERIPH_LCD_CAM_MODULE); @@ -154,7 +125,7 @@ bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16B // Calculate clock divider double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; - + LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { @@ -165,7 +136,7 @@ bool LcdBusContext::init(const LedTiming& timing, size_t bufferSize, bool use16B 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) { @@ -267,10 +238,56 @@ void LcdBusContext::deinit() { periph_module_disable(PERIPH_LCD_CAM_MODULE); } + _dmaAllocated = false; _initialized = false; } -int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus, bool inverted) { +// Compute the DMA buffer size from _maxSrcBytes (set during registerChannel() by the largest bus), +// then allocate the circular DMA descriptor/buffer chain. +// Called once from startTransmit() after all channels have registered. +bool LcdBusContext::_allocDmaBuffers() { + if (_dmaBuffer[0] != nullptr) return true; // already allocated + + size_t dmaBytesPerSrc = _use16Bit ? 64 : 32; + _bufferSize = (dmaBytesPerSrc * _maxSrcBytes) / WLEDPB_LCD_DMA_BUFFER_COUNT; + _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes + if (_bufferSize > WLEDPB_LCD_DMA_BUFFER_SIZE) _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; + if (_bufferSize < 1024) _bufferSize = 1024; // MIN_DMA_BUFFER_SIZE + + for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { + _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); + if (!_dmaBuffer[i]) { + LCD_LOG("ERROR: DMA buffer %d alloc failed", i); + deinit(); + return false; + } + memset(_dmaBuffer[i], 0, _bufferSize); + + _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); + if (!_dmaDesc[i]) { + LCD_LOG("ERROR: DMA desc %d alloc failed", i); + deinit(); + return false; + } + memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); + } + + // Setup DMA descriptors in CIRCULAR mode (restored per-transmission in startTransmit) + for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { + _dmaDesc[i]->dw0.size = _bufferSize; + _dmaDesc[i]->dw0.length = _bufferSize; + _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR + _dmaDesc[i]->buffer = _dmaBuffer[i]; + _dmaDesc[i]->next = _dmaDesc[(i + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT]; // circular chain + } + + _dmaAllocated = true; + LCD_LOG("DMA buffers allocated: bufSize=%u x%u", (unsigned)_bufferSize, WLEDPB_LCD_DMA_BUFFER_COUNT); + return true; +} + +int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus, size_t srcBytes, bool inverted) { int8_t idx = -1; for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { if (!_channels[i].active) { @@ -286,11 +303,13 @@ int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus, bool inverted) { _channelCount++; _channelMask |= (1 << idx); + if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; + esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, inverted, false); gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); - LCD_LOG("Channel %d: pin=%d, mask=0x%04X", idx, pin, _channelMask); + LCD_LOG("Channel %d: pin=%d, mask=0x%04X, srcBytes=%u", idx, pin, _channelMask, (unsigned)srcBytes); return idx; } @@ -322,18 +341,13 @@ void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ #ifdef WLED_PIXELBUS_16PARALLEL // 16-bit parallel encode: branchless gather + scatter, 64 bytes per source byte (16 channels) -void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; - } - +void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { 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 (int ch = 0; ch < maxCh; ch++) { + for (int ch = 0; ch < maxChannel; ch++) { if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; const uint16_t m = (uint16_t)(1u << ch); @@ -351,8 +365,6 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { if (!alwaysMask) break; // S3 LCD: no byte swapping. - // Output cadence [HIGH,data,data,LOW] -> as 32-bit pairs: - // p[0] = (bN<<16)|alwaysMask, p[1] = (0<<16)|bN uint32_t* p = (uint32_t*)(dest + pos); #define EMIT(bN, OFF) \ p[OFF] = ((uint32_t)(bN) << 16) | alwaysMask; \ @@ -366,18 +378,13 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { #else // WLED_PIXELBUS_16PARALLEL // 8-bit parallel encode: branchless gather + scatter, 32 bytes per source byte (8 channels) -void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; - } - +void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { uint8_t alwaysMask = 0; uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; - for (int ch = 0; ch < maxCh; ch++) { + for (int ch = 0; ch < maxChannel; ch++) { if (!_channels[ch].active) continue; if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; const uint8_t m = (uint8_t)(1u << ch); @@ -394,10 +401,6 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { } if (!alwaysMask) break; - // S3 LCD 8-bit mode (lcd_2byte_en=0): no byte swapping, output [S0,S1,S2,S3]. - // Output cadence [HIGH,data,data,LOW]: - // As 4 bytes: [alwaysMask, bN, bN, 0] - // As uint32_t (LE): alwaysMask | (bN<<8) | (bN<<16) | 0 uint32_t* p = (uint32_t*)(dest + pos); #define EMIT8(bN, OFF) \ p[OFF] = (uint32_t)(alwaysMask) | ((uint32_t)(bN) << 8) | ((uint32_t)(bN) << 16); @@ -409,88 +412,87 @@ void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen) { #endif // WLED_PIXELBUS_16PARALLEL -/* -// 3-step cadence implementation, currently unused -#ifdef WLED_PIXELBUS_16PARALLEL -void IRAM_ATTR LcdBusContext::encode3Step(uint8_t* dest, size_t destLen) { - // 3-step cadence encoding for 16-bit parallel output - // Each source bit becomes 3 DMA words: [HIGH][data][LOW] - memset(dest, 0, destLen); - size_t pos = 0; +void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { + memset(_dmaBuffer[bufIdx], 0, _bufferSize); - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) maxCh = ch + 1; + if (_resetBytesLeft > 0) { + _dmaDesc[bufIdx]->dw0.length = _resetBytesLeft; + _dmaDesc[bufIdx]->next = nullptr; // stop the engine after this one + _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers + return; } - while (pos + 48 <= destLen) { // 8 bits * 3 steps * 2 bytes = 48 bytes per source byte - bool hasData = false; - - for (int ch = 0; ch < maxCh; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - - hasData = true; - uint8_t srcByte = _channels[ch].srcData[_channels[ch].srcPos]; - uint16_t chMask = (1 << ch); - - uint16_t* p = (uint16_t*)(dest + pos); - - // Unrolled loop for 8 bits - // [HIGH][data][LOW] — step 0 always high, step 1 = data bit, step 2 always low (zero from memset) - p[0] |= chMask; if (srcByte & 0x80) { p[1] |= chMask; } p += 3; // bit 7 - p[0] |= chMask; if (srcByte & 0x40) { p[1] |= chMask; } p += 3; - p[0] |= chMask; if (srcByte & 0x20) { p[1] |= chMask; } p += 3; - p[0] |= chMask; if (srcByte & 0x10) { p[1] |= chMask; } p += 3; - p[0] |= chMask; if (srcByte & 0x08) { p[1] |= chMask; } p += 3; - p[0] |= chMask; if (srcByte & 0x04) { p[1] |= chMask; } p += 3; - p[0] |= chMask; if (srcByte & 0x02) { p[1] |= chMask; } p += 3; - p[0] |= chMask; if (srcByte & 0x01) { p[1] |= chMask; } p += 3; // bit 0 + uint32_t bytesToEncode = 0; + uint8_t maxCh = 0; + for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { + if (_channels[ch].active) { + maxCh = ch + 1; + uint32_t channelBytesLeft = _channels[ch].srcLen - _channels[ch].srcPos; + if (channelBytesLeft > bytesToEncode) bytesToEncode = channelBytesLeft; } + } - if (!hasData) break; - - for (int ch = 0; ch < maxCh; ch++) { - if (_channels[ch].active && _channels[ch].srcPos < _channels[ch].srcLen) { - _channels[ch].srcPos++; - } + size_t dmaBytesPerSrc = _use16Bit ? 64 : 32; + uint32_t translatedbytes = bytesToEncode * dmaBytesPerSrc; + translatedbytes = translatedbytes > _bufferSize ? _bufferSize : translatedbytes; + encode4Step(_dmaBuffer[bufIdx], translatedbytes, maxCh); + + if (translatedbytes < _bufferSize) { + // Data ran out before the buffer was full (i.e. we are done), compute the minimum reset period we must send as zero cycles + uint32_t resetNs = _timing.reset_us * 1000; + uint32_t bitPeriodNs = _timing.bitPeriod() + 1; // +1 to ensure no division by zero and slightly over-estimate the reset cycle + uint32_t zeroCycles = resetNs / bitPeriodNs; + size_t resetBytes = zeroCycles * (dmaBytesPerSrc / 8); // one cycle is 4 clocks, on each clock one/two buffer byte(s) sent out in parallel + + size_t newLen = translatedbytes + resetBytes; + if (newLen > _bufferSize) { + _resetBytesLeft = newLen - _bufferSize; // reset pulse does not fit into this buffer frame, send another one + _dmaDesc[bufIdx]->dw0.length = _bufferSize; + } else { + _dmaDesc[bufIdx]->dw0.length = newLen; // send the rest (zeroes) as a reset + _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers or it will mess up the DMA + _dmaDesc[bufIdx]->next = nullptr; // reset fit into this buffer, end transfer after this is sent } - - pos += 48; } } -#endif // WLED_PIXELBUS_16PARALLEL (encode3Step) -*/ - -void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { - encode4Step(_dmaBuffer[bufIdx], _bufferSize); -} bool LcdBusContext::startTransmit() { if (_state != DriverState::Idle) return false; if (_channelCount == 0) return false; - if (_stagedMask != _channelMask) return true; // wait for all channels - _stagedMask = 0; + // Only start transmission if ALL active channels have populated data + if (_stagedMask != _channelMask) return true; + _stagedMask = 0; // Reset for next frame - // Reset channel positions _maxDataLen = 0; for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { if (_channels[ch].active) { _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) _maxDataLen = _channels[ch].srcLen; + if (_channels[ch].srcLen > _maxDataLen) { + _maxDataLen = _channels[ch].srcLen; + } } } if (_maxDataLen == 0) return false; - // Fill all buffers initially + _resetBytesLeft = 0; + + // allocate or reallocate DMA buffers if needed — deferred from init() so all channels + // can register first and _maxSrcBytes reflects the bus with the most pixels + if (!_dmaAllocated) { + if (!_allocDmaBuffers()) return false; + } + + // Fill all buffers initially and restore circular chain for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { + _dmaDesc[i]->next = _dmaDesc[(i + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT]; // restore circular buffer chain + _dmaDesc[i]->dw0.length = _bufferSize; fillBuffer(i); - _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmaDesc[i]->dw0.suc_eof = 1; // enable eof, just in case + _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; // hand ownership over to DMA after descriptor init } - _activeBuffer = 0; - _remainingDataBuffers = WLEDPB_LCD_DMA_BUFFER_COUNT; + _activeBuffer = 0; // start with first buffer _state = DriverState::Sending; // Start DMA (circular mode - descriptors already linked) @@ -516,37 +518,17 @@ IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, // The completed buffer just finished playing; DMA is now on the next buffer uint8_t completedBuf = ctx->_activeBuffer; ctx->_activeBuffer = (ctx->_activeBuffer + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT; - memset(ctx->_dmaBuffer[completedBuf], 0, ctx->_bufferSize); // pre-clear; will be refilled or left as reset signal - - if (ctx->_state == DriverState::Sending) { - ctx->fillBuffer(completedBuf); - bool moreData = false; - for (int ch = 0; ch < WLEDPB_LCD_MAX_CHANNELS; ch++) { - if (ctx->_channels[ch].active && - ctx->_channels[ch].srcPos < ctx->_channels[ch].srcLen) { - moreData = true; - break; - } - } - if (!moreData) { - ctx->_remainingDataBuffers = WLEDPB_LCD_DMA_BUFFER_COUNT; - ctx->_state = DriverState::SendingLast; - } - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - - } else if (ctx->_state == DriverState::SendingLast) { - // completedBuf already zeroed above — acts as reset buffer - if (ctx->_remainingDataBuffers > 0) ctx->_remainingDataBuffers--; - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - if (ctx->_remainingDataBuffers == 0) { - ctx->_state = DriverState::WaitingReset; - } - } else { - // WaitingReset complete — stop DMA + if (ctx->_dmaDesc[completedBuf]->next == nullptr) { // this was the last buffer LCD_CAM.lcd_user.lcd_start = 0; gdma_stop(ctx->_dmaChannel); ctx->_state = DriverState::Idle; + return false; + } + + if (ctx->_resetBytesLeft != 3) { + ctx->fillBuffer(completedBuf); // fill buffer, handle reset pulse and end of frame + ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; // owner is reset upon eof and must be handed back to DMA } return false; // Do not yield OS for this DMA streaming interrupt @@ -556,10 +538,8 @@ IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, // LcdBus Implementation // ============================================ -LcdBus::LcdBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, - size_t bufferSize, bool use16Bit, uint8_t ledType) +LcdBus::LcdBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, size_t numPixels, bool use16Bit, uint8_t ledType) : _pin(pin) - , _bufferSize(bufferSize) , _use16Bit(use16Bit) , _timing(timing) , _initialized(false) @@ -580,13 +560,15 @@ bool LcdBus::begin() { _ctx = LcdBusContext::get(); if (!_ctx) return false; - if (!_ctx->init(_timing, _bufferSize, _use16Bit)) { + if (!_ctx->init(_timing, _use16Bit)) { LcdBusContext::release(); _ctx = nullptr; return false; } - _channelIdx = _ctx->registerChannel(_pin, this, _inverted); + // pass our encoded byte count so the context can size DMA buffers for the largest bus + const size_t srcBytes = (size_t)_numPixels * _encoder.getPixelBytes(); + _channelIdx = _ctx->registerChannel(_pin, this, srcBytes, _inverted); if (_channelIdx < 0) { LcdBusContext::release(); _ctx = nullptr; @@ -611,13 +593,15 @@ void LcdBus::end() { if (!_initialized) return; if (_ctx) { + // Wait for any active transmission to complete before cleanup + while (!_ctx->isIdle()) vTaskDelay(1); _ctx->unregisterChannel(_channelIdx); LcdBusContext::release(); _ctx = nullptr; } if (_encodeBuffer) { - free(_encodeBuffer); + heap_caps_free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } @@ -648,8 +632,6 @@ bool LcdBus::canShow() const { return _ctx->isIdle(); } - - void LcdBus::setColorOrder(uint8_t co) { _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h index 4111c451f5..ab94485069 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h @@ -77,10 +77,10 @@ class LcdBusContext { static LcdBusContext* get(); static void release(); - bool init(const LedTiming& timing, size_t bufferSize, bool use16Bit = false); + bool init(const LedTiming& timing, bool use16Bit = false); void deinit(); - int8_t registerChannel(int8_t pin, LcdBus* bus, bool inverted = false); + int8_t registerChannel(int8_t pin, LcdBus* bus, size_t srcBytes, bool inverted = false); void unregisterChannel(int8_t channelIdx); uint8_t getChannelCount() const { return _channelCount; } @@ -99,12 +99,10 @@ class LcdBusContext { LcdBusContext(); ~LcdBusContext(); - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen); + void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); void fillBuffer(uint8_t bufIdx); - - static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, - gdma_event_data_t* event_data, - void* user_data); + bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); volatile DriverState _state; bool _initialized; @@ -114,12 +112,15 @@ class LcdBusContext { gdma_channel_handle_t _dmaChannel; dma_descriptor_t* _dmaDesc[WLEDPB_LCD_DMA_BUFFER_COUNT]; uint8_t* _dmaBuffer[WLEDPB_LCD_DMA_BUFFER_COUNT]; - volatile uint8_t _activeBuffer; - volatile uint8_t _remainingDataBuffers; // Timing LedTiming _timing; size_t _bufferSize; + size_t _maxSrcBytes; // max source (encoded pixel) bytes across all registered channels; drives DMA sizing + bool _dmaAllocated; // true when DMA buffers are allocated and reflect current _maxSrcBytes + volatile uint8_t _activeBuffer; + volatile uint8_t _remainingDataBuffers; + volatile uint16_t _resetBytesLeft; // Channels struct ChannelData { @@ -136,15 +137,15 @@ class LcdBusContext { uint16_t _stagedMask; size_t _maxDataLen; + static LcdBusContext* _instance; static uint8_t _refCount; friend class LcdBus; }; class LcdBus : public PixelBus { -public: - LcdBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, - size_t bufferSize = DEFAULT_DMA_BUFFER_SIZE, bool use16Bit = false, uint8_t ledType = 0); +public: + LcdBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, size_t numPixels, bool use16Bit = false, uint8_t ledType = 0); ~LcdBus() override; bool begin() override; @@ -162,7 +163,6 @@ class LcdBus : public PixelBus { private: int8_t _pin; - size_t _bufferSize; bool _use16Bit; LedTiming _timing; bool _inverted = false; From 4a44e9ecf4b3acaf1300cda889ca9a50bc8c4060 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 1 Jul 2026 00:35:43 +0200 Subject: [PATCH 167/173] merge I2S bus and LCD bus code, some cleanup --- wled00/bus_wrapper.h | 7 +- wled00/const.h | 3 +- wled00/data/settings_leds.htm | 2 + wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 9 +- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 29 +- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp | 473 +++++++++----- wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h | 94 ++- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp | 640 ------------------- wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h | 177 ----- 9 files changed, 382 insertions(+), 1052 deletions(-) delete mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp delete mode 100644 wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index 072a690e7a..e68d9f6b0e 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -8,7 +8,6 @@ #if defined(ARDUINO_ARCH_ESP32) #include "src/WLEDpixelBus/WLEDpixelBus_RMT.h" #include "src/WLEDpixelBus/WLEDpixelBus_I2S.h" -#include "src/WLEDpixelBus/WLEDpixelBus_LCD.h" #include "src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h" #elif defined(ARDUINO_ARCH_ESP8266) #include "src/WLEDpixelBus/WLEDpixelBus_ESP8266.h" @@ -158,10 +157,8 @@ static WLEDpixelBus::PixelBus* create(uint8_t busType, uint8_t* pins, uint16_t l switch (driverType) { case BUSDRV_RMT: driver = WLEDpixelBus::BusDriver::RMT; break; case BUSDRV_I2S: - #if defined(CONFIG_IDF_TARGET_ESP32S3) - driver = WLEDpixelBus::BusDriver::LCD; // S3 has LCD peripheral with 16 channels, very similar to I2S - #elif defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) - driver = WLEDpixelBus::BusDriver::I2S; + #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + driver = WLEDpixelBus::BusDriver::I2S; // uses LCD hardware on S3 no actually I2S #elif defined(CONFIG_IDF_TARGET_ESP32C3) driver = WLEDpixelBus::BusDriver::SPI; // parallel SPI #else diff --git a/wled00/const.h b/wled00/const.h index a0ab963ef3..56645ba024 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -347,7 +347,8 @@ constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - #define TYPE_TM1914 33 //RGB #define TYPE_SM16825 34 //RGB + WW + CW #define TYPE_TM1815 35 //RGBW (half speed TM1814) -#define TYPE_CUSTOM_BUS 36 // fully configurable single-wire digital type (1-6 channels, custom per-channel color mapping) +//TODO add support for SM16714, see https://wled.discourse.group/t/sm16714-pixel-ic/9794 +#define TYPE_CUSTOM_BUS 39 // fully configurable single-wire digital type (1-6 channels, custom per-channel color mapping) #define TYPE_DIGITAL_MAX 39 // last usable digital type //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 9815d75cad..5b9cee8d83 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -539,6 +539,7 @@ chanuse.style.color = '#ccc'; channelMsg.textContent = `Hardware channels used: RMT ${usage.rmtUsed}/${maxRMT}, I2S ${usage.i2sUsed}/${maxI2S}` + (usage.bbUsed > 0 ? `, BitBang ${usage.bbUsed}/${maxBB}` : ''); if (isC3()) channelMsg.textContent = channelMsg.textContent.replace("I2S", "SPI"); // replace I2S with "SPI" on C3 + if (isS3()) channelMsg.textContent = channelMsg.textContent.replace("I2S", "LCD"); // replace I2S with "LCD" on S3 if (usage.rmtUsed > maxRMT || usage.i2sUsed > maxI2S || usage.bbUsed > maxBB) { chanuse.style.color = 'red'; } @@ -1109,6 +1110,7 @@ let i2sOpt = drvsel.querySelector('option[value="1"]'); let bbOpt = drvsel.querySelector('option[value="2"]'); if (isC3()) i2sOpt.textContent = "SPI"; // rename to SPI on C3 + if (isS3()) i2sOpt.textContent = "LCD"; // rename to LCD on S3 rmtOpt.disabled = false; i2sOpt.disabled = false; if (bbOpt) bbOpt.disabled = false; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 6698aaf87b..204c6c43bf 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -26,7 +26,6 @@ Currently based on IDF v4.x API functions and low-level HAL #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) #include "WLEDpixelBus_RMT.h" #include "WLEDpixelBus_I2S.h" -#include "WLEDpixelBus_LCD.h" #include "WLEDpixelBus_ParallelSpi.h" #endif @@ -222,7 +221,7 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 #if defined(CONFIG_IDF_TARGET_ESP32C3) #error C3 hardware does not support parallel I2S output and single channel output is not implemented, use WLEDPB_PARALLEL_SPI_SUPPORT instead #endif -#if defined(CONFIG_IDF_TARGET_ESP32S2) +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) bus = new I2sBus(pin, timing, colorOrder, numChannels, 0, ledType, numPixels); #else bus = new I2sBus(pin, timing, colorOrder, numChannels, 1, ledType, numPixels); @@ -230,12 +229,6 @@ PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8 break; #endif -#ifdef WLEDPB_LCD_SUPPORT - case BusDriver::LCD: - bus = new LcdBus(pin, timing, colorOrder, numChannels, numPixels, false, ledType); - break; -#endif - #ifdef WLEDPB_PARALLEL_SPI_SUPPORT case BusDriver::SPI: bus = new ParallelSpiBus(pin, timing, colorOrder, numChannels, ledType); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index c1cebd1f28..ebcb20a32e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -37,18 +37,12 @@ Currently based on IDF v4.x API functions and low-level HAL #include "driver/rmt.h" // I2S support: targets where the I2S peripheral has the LCD Intel 8080 mode -// used for parallel LED output. SOC_I2S_LCD_I80_VARIANT is defined on ESP32 and -// ESP32-S2; absent on S3 (which has a dedicated LCD CAM) and C3. -#if SOC_I2S_LCD_I80_VARIANT +// used for parallel LED output. SOC_I2S_LCD_I80_VARIANT is defined on ESP32 and ESP32-S2 only +// SOC_LCDCAM_SUPPORTED is defined on ESP32-S3 only (uses LCD peripheral, not actually I2S) +#if (SOC_I2S_LCD_I80_VARIANT || SOC_LCDCAM_SUPPORTED) #define WLEDPB_I2S_SUPPORT #endif -// LCD support: targets with a dedicated LCD CAM peripheral. -// SOC_LCDCAM_SUPPORTED is defined on ESP32-S3 only. -#if SOC_LCDCAM_SUPPORTED - #define WLEDPB_LCD_SUPPORT -#endif - // SPI parallel support (C3 - uses SPI quad mode with GDMA) #if defined(CONFIG_IDF_TARGET_ESP32C3) #define WLEDPB_PARALLEL_SPI_SUPPORT @@ -517,23 +511,17 @@ class I2sBus; class I2sBusContext; #endif -#ifdef WLEDPB_LCD_SUPPORT -class LcdBus; -class LcdBusContext; -#endif - //============================================================================== // Bus Factory - Create appropriate bus for platform //============================================================================== enum class BusDriver : uint8_t { RMT = 0, - I2S = 1, - LCD = 2, - SPI = 3, - UART = 4, - DMA = 5, - BitBang = 6 + I2S = 1, // I2S on ESP32 and S2, LCD on S3 + SPI = 2, // parallel SPI output + UART = 3, + DMA = 4, + BitBang = 5 }; /** @@ -573,7 +561,6 @@ PixelBus* createBus(BusDriver type, int8_t pin, const LedTiming& timing, #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) #include "WLEDpixelBus_RMT.h" #include "WLEDpixelBus_I2S.h" -#include "WLEDpixelBus_LCD.h" #include "WLEDpixelBus_ParallelSpi.h" #include "WLEDpixelBus_BitBang.h" #elif defined(ESP8266) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp index bf05724144..fb80f0840a 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.cpp @@ -1,12 +1,12 @@ /*------------------------------------------------------------------------- -WLEDpixelBus - parallel I2S output driver implementation +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 and ESP32 S2 +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 @@ -17,17 +17,11 @@ Each bus can have individual configuration of color channels but all must share -------------------------------------------------------------------------*/ - -#include "WLEDpixelBus.h" -#ifdef WLEDPB_I2S_SUPPORT #include "WLEDpixelBus_I2S.h" +#ifdef WLEDPB_I2S_SUPPORT namespace WLEDpixelBus { -//============================================================================== -// I2S Bus Implementation -//============================================================================== - I2sBusContext* I2sBusContext::_instances[WLEDPB_I2S_BUS_COUNT] = {nullptr}; uint8_t I2sBusContext::_refCount[WLEDPB_I2S_BUS_COUNT] = {0}; @@ -53,8 +47,21 @@ void I2sBusContext::release(uint8_t busNum) { } } +#ifdef CONFIG_IDF_TARGET_ESP32S3 +I2sBusContext::I2sBusContext(uint8_t /*busNum*/) + : _dmaChannel(nullptr) +#else I2sBusContext::I2sBusContext(uint8_t busNum) : _busNum(busNum) + , _i2sDev( +#if defined(CONFIG_IDF_TARGET_ESP32) + (busNum == 0) ? &I2S0 : &I2S1 +#else + &I2S0 +#endif + ) + , _isrHandle(nullptr) +#endif , _state(DriverState::Idle) , _initialized(false) , _maxSrcBytes(0) @@ -65,18 +72,11 @@ I2sBusContext::I2sBusContext(uint8_t busNum) , _resetBytesLeft(0) , _timing{0, 0, 0, 0, 0} , _clockDiv(1) - , _isrHandle(nullptr) , _channelCount(0) , _channelMask(0) , _stagedMask(0) , _maxDataLen(0) { -#if defined(CONFIG_IDF_TARGET_ESP32) - _i2sDev = (busNum == 0) ? &I2S0 : &I2S1; -#else - _i2sDev = &I2S0; -#endif - for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; } @@ -87,19 +87,169 @@ I2sBusContext::I2sBusContext(uint8_t busNum) } } -I2sBusContext:: ~I2sBusContext() { +I2sBusContext::~I2sBusContext() { deinit(); } bool I2sBusContext::init(const LedTiming& timing) { if (_initialized) return true; - //pinMode(33, OUTPUT); // debug pin for timing analysis - _timing = timing; - // DMA buffer allocation is deferred to _allocDmaBuffers(), called from startTransmit() once - // all channels have registered and _maxSrcBytes reflects the largest bus. + if (!hwInit(timing)) { + deinit(); + return false; + } + + _initialized = true; + return true; +} + +void I2sBusContext::deinit() { + int timeout = 100; + while (!isIdle() && timeout--) { vTaskDelay(1); } + + hwStopTransfer(); + + for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { + if (_dmaBuffer[i]) { + heap_caps_free(_dmaBuffer[i]); + _dmaBuffer[i] = nullptr; + } + if (_dmaDesc[i]) { + heap_caps_free(_dmaDesc[i]); + _dmaDesc[i] = nullptr; + } + } + + hwDeinit(); + + _dmaAllocated = false; + _initialized = false; +} + +//============================================================================== +// Hardware abstraction +//============================================================================== + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + +bool I2sBusContext::hwInit(const LedTiming& timing) { + uint32_t bitPeriodNs = timing.bitPeriod(); + + // Enable LCD_CAM peripheral + periph_module_enable(PERIPH_LCD_CAM_MODULE); + periph_module_reset(PERIPH_LCD_CAM_MODULE); + // Reset LCD + LCD_CAM.lcd_user.lcd_reset = 1; + esp_rom_delay_us(100); + LCD_CAM.lcd_user.lcd_reset = 0; + esp_rom_delay_us(100); + + // Calculate clock divider for 4-step cadence + double clkm_div = (double)bitPeriodNs / 4.0 / 1000.0 * 240.0; + if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { + return false; + } + + uint8_t clkm_div_int = (uint8_t)clkm_div; + double clkm_frac = clkm_div - clkm_div_int; + uint8_t divB = 0; + uint8_t divA = 0; + if (clkm_frac > 0.001) { + divA = 63; + divB = (uint8_t)(clkm_frac * 63.0 + 0.5); + if (divB >= divA) divB = divA - 1; + } + + // Configure LCD clock + LCD_CAM.lcd_clock.clk_en = 1; + LCD_CAM.lcd_clock.lcd_clk_sel = 2; + LCD_CAM.lcd_clock.lcd_clkm_div_a = divA; + LCD_CAM.lcd_clock.lcd_clkm_div_b = divB; + LCD_CAM.lcd_clock.lcd_clkm_div_num = clkm_div_int; + LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; + LCD_CAM.lcd_clock.lcd_ck_idle_edge = 0; + LCD_CAM.lcd_clock.lcd_clk_equ_sysclk = 1; + + // Configure frame format + LCD_CAM.lcd_ctrl.lcd_rgb_mode_en = 0; + LCD_CAM.lcd_rgb_yuv.lcd_conv_bypass = 0; + LCD_CAM.lcd_misc.lcd_next_frame_en = 0; + LCD_CAM.lcd_data_dout_mode.val = 0; + LCD_CAM.lcd_user.lcd_always_out_en = 1; + LCD_CAM.lcd_user.lcd_8bits_order = 0; + LCD_CAM.lcd_user.lcd_bit_order = 0; +#ifdef WLED_PIXELBUS_16PARALLEL + LCD_CAM.lcd_user.lcd_2byte_en = 1; +#else + LCD_CAM.lcd_user.lcd_2byte_en = 0; +#endif + LCD_CAM.lcd_user.lcd_dummy = 1; + LCD_CAM.lcd_user.lcd_dummy_cyclelen = 0; + LCD_CAM.lcd_user.lcd_cmd = 0; + + // Allocate GDMA channel + gdma_channel_alloc_config_t dma_chan_config = { + .sibling_chan = NULL, + .direction = GDMA_CHANNEL_DIRECTION_TX, + .flags = {.reserve_sibling = 0} + }; + + esp_err_t err = gdma_new_channel(&dma_chan_config, &_dmaChannel); + if (err != ESP_OK) return false; + + err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + if (err != ESP_OK) return false; + + gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(_dmaChannel, &strategy_config); + + // Register DMA callback + gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; + gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); + + return true; +} + +void I2sBusContext::hwDeinit() { + if (_dmaChannel) { + gdma_disconnect(_dmaChannel); + gdma_del_channel(_dmaChannel); + _dmaChannel = nullptr; + } + periph_module_disable(PERIPH_LCD_CAM_MODULE); +} + +void I2sBusContext::hwStartTransfer() { + gdma_reset(_dmaChannel); + LCD_CAM.lcd_user.lcd_dout = 1; + LCD_CAM.lcd_user.lcd_update = 1; + LCD_CAM.lcd_misc.lcd_afifo_reset = 1; + LCD_CAM.lcd_misc.lcd_afifo_reset = 0; + gdma_start(_dmaChannel, (intptr_t)_dmaDesc[0]); + esp_rom_delay_us(1); + LCD_CAM.lcd_user.lcd_start = 1; +} + +void IRAM_ATTR I2sBusContext::hwStopTransfer() { + LCD_CAM.lcd_user.lcd_start = 0; + if (_dmaChannel) gdma_stop(_dmaChannel); +} + +void I2sBusContext::hwRoutePin(int8_t pin, int8_t idx, bool inverted) { + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, inverted, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); +} + +#else // !CONFIG_IDF_TARGET_ESP32S3 + +bool I2sBusContext::hwInit(const LedTiming& timing) { // Enable I2S peripheral #if defined(CONFIG_IDF_TARGET_ESP32) periph_module_enable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); @@ -163,10 +313,8 @@ bool I2sBusContext::init(const LedTiming& timing) { // For ESP32 Classic, use 16-bit FIFO mode #if !defined(CONFIG_IDF_TARGET_ESP32S2) _i2sDev->fifo_conf.tx_fifo_mod = 1; - //_i2sDev->conf_chan.tx_chan_mod = 0; // Standard mode #else _i2sDev->fifo_conf.tx_fifo_mod = 3; - //_i2sDev->conf_chan.tx_chan_mod = 1; #endif _i2sDev->fifo_conf.tx_data_num = 32; // FIFO threshold @@ -228,11 +376,6 @@ bool I2sBusContext::init(const LedTiming& timing) { _clockDiv = clkmInteger; - //Serial.printf("[I2S] Clock: bitPeriod=%uns, clkm_div=%u+%u/%u, bck_div=%u\n", bitPeriodNs, clkmInteger, divB, divA, bckDiv); - double actualStepNs = (double)clkmInteger * bckDiv / baseClockMhz * 1000.0; - if (divA > 0) actualStepNs = ((double)clkmInteger + (double)divB / divA) * bckDiv / baseClockMhz * 1000.0; - //Serial.printf("[I2S] Step time: %.1fns (target: %.1fns), bit period: %.1fns\n", actualStepNs, (double)bitPeriodNs / 4.0, actualStepNs * 4.0); - // Set clock (with fractional divider for accurate timing) _i2sDev->clkm_conf.val = 0; @@ -248,13 +391,12 @@ bool I2sBusContext::init(const LedTiming& timing) { _i2sDev->clkm_conf.clkm_div_b = divB; _i2sDev->clkm_conf.clkm_div_num = clkmInteger; - // Sample rate - bck must be >= 2 _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 + _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 + _i2sDev->sample_rate_conf.tx_bits_mod = 8; // 8-bit samples for up to 8 parallel channels #endif // Final reset before ISR install @@ -281,59 +423,90 @@ bool I2sBusContext::init(const LedTiming& timing) { 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) { - //DEBUG_PRINTF("I2S ISR alloc failed: %d", err); deinit(); return false; } - _initialized = true; - //DEBUG_PRINTF("[I2S] Init complete: bus=%u, bufSize=%u\n", _busNum, _bufferSize); return true; } -void I2sBusContext::deinit() { - // wait for finish sending before deinit (just in case) - int timeout = 100; - while (!isIdle() && timeout--) { vTaskDelay(1); } - - if (_i2sDev) { - _i2sDev->int_ena.val = 0; // Disable interrupts first - _i2sDev->conf.tx_start = 0; - _i2sDev->out_link.start = 0; - } - +void I2sBusContext::hwDeinit() { if (_isrHandle) { esp_intr_free(_isrHandle); _isrHandle = nullptr; } - - 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; - } - } - #if defined(CONFIG_IDF_TARGET_ESP32) - periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); + periph_module_disable(_busNum == 0 ? PERIPH_I2S0_MODULE : PERIPH_I2S1_MODULE); #else periph_module_disable(PERIPH_I2S0_MODULE); #endif +} - _dmaAllocated = false; - _initialized = false; +void I2sBusContext::hwStartTransfer() { + _i2sDev->lc_conf.in_rst = 1; + _i2sDev->lc_conf.out_rst = 1; + _i2sDev->lc_conf.ahbm_rst = 1; + _i2sDev->lc_conf.ahbm_fifo_rst = 1; + _i2sDev->lc_conf.in_rst = 0; + _i2sDev->lc_conf.out_rst = 0; + _i2sDev->lc_conf.ahbm_rst = 0; + _i2sDev->lc_conf.ahbm_fifo_rst = 0; + _i2sDev->conf.tx_reset = 1; + _i2sDev->conf.tx_fifo_reset = 1; + _i2sDev->conf.tx_reset = 0; + _i2sDev->conf.tx_fifo_reset = 0; + + _i2sDev->int_clr.val = 0xFFFFFFFF; + _i2sDev->int_ena.out_eof = 1; + + _i2sDev->fifo_conf.dscr_en = 1; + _i2sDev->out_link.start = 0; + _i2sDev->out_link.addr = (uint32_t)_dmaDesc[0]; + _i2sDev->out_link.start = 1; + _i2sDev->conf.tx_start = 1; } -// Compute the DMA buffer size from _maxSrcBytes (set during registerChannel() by the largest bus), -// then allocate the circular DMA descriptor/buffer chain. -// Called once from startTransmit() after all channels have registered. +void IRAM_ATTR I2sBusContext::hwStopTransfer() { + if (_i2sDev) { + _i2sDev->int_ena.out_eof = 0; + _i2sDev->conf.tx_start = 0; + _i2sDev->out_link.start = 0; + } +} + +void I2sBusContext::hwRoutePin(int8_t pin, int8_t idx, bool inverted) { + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); // Configure GPIO + int sigIdx; +#ifdef WLED_PIXELBUS_16PARALLEL + #if defined(CONFIG_IDF_TARGET_ESP32) + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT8_IDX : I2S1O_DATA_OUT8_IDX; + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + sigIdx = I2S0O_DATA_OUT8_IDX; // 16-bit mode: mapping starts at DATA_OUT8_IDX for the wide 16-bit window + #else + sigIdx = I2S0O_DATA_OUT0_IDX; + #endif +#else + #if defined(CONFIG_IDF_TARGET_ESP32) + sigIdx = (_busNum == 0) ? I2S0O_DATA_OUT0_IDX : I2S1O_DATA_OUT0_IDX; + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + sigIdx = I2S0O_DATA_OUT16_IDX; // 8-bit parallel maps to upper bytes on S2 + #else + sigIdx = I2S0O_DATA_OUT0_IDX; + #endif +#endif + sigIdx += idx; + esp_rom_gpio_connect_out_signal(pin, sigIdx, inverted, false); +} + +#endif // !CONFIG_IDF_TARGET_ESP32S3 + +//============================================================================== +// Buffer management & encoding +//============================================================================== + bool I2sBusContext::_allocDmaBuffers() { - // DMA buffer size: spread total pixel data evenly across the circular buffer slots, - // then clamp to [MIN_DMA_BUFFER_SIZE, DEFAULT_DMA_BUFFER_SIZE] + if (_dmaBuffer[0] != nullptr) return true; + _bufferSize = (WLEDPB_I2S_DMABYTES * _maxSrcBytes) / WLEDPB_I2S_DMA_BUFFER_COUNT; _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes if (_bufferSize > DEFAULT_DMA_BUFFER_SIZE) _bufferSize = DEFAULT_DMA_BUFFER_SIZE; @@ -345,19 +518,17 @@ bool I2sBusContext::_allocDmaBuffers() { if (!_dmaBuffer[i]) return false; memset(_dmaBuffer[i], 0, _bufferSize); - _dmaDesc[i] = (lldesc_t*)heap_caps_aligned_alloc(4, sizeof(lldesc_t), MALLOC_CAP_DMA); + _dmaDesc[i] = (DmaDesc_t*)heap_caps_aligned_alloc(4, sizeof(DmaDesc_t), MALLOC_CAP_DMA); if (!_dmaDesc[i]) return false; } // set up DMA descriptors as a circular linked list for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i]->size = _bufferSize; - _dmaDesc[i]->length = _bufferSize; - _dmaDesc[i]->buf = _dmaBuffer[i]; - _dmaDesc[i]->eof = 1; // generate EOF interrupt on completion of each buffer - _dmaDesc[i]->sosf = 0; - _dmaDesc[i]->owner = 1; // hand ownership to DMA engine - _dmaDesc[i]->qe.stqe_next = _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]; + 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; @@ -369,7 +540,7 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, size_t srcBytes, // Find free slot int8_t idx = -1; for (int i = 0; i < WLEDPB_I2S_MAX_CHANNELS; i++) { - if (! _channels[i].active) { + if (!_channels[i].active) { idx = i; break; } @@ -386,34 +557,7 @@ int8_t I2sBusContext::registerChannel(int8_t pin, I2sBus* bus, size_t srcBytes, // track the largest source byte count across channels; used in _allocDmaBuffers() to size DMA buffers if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; - // Configure GPIO - gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); - - // Route I2S output to GPIO - int sigIdx; -#ifdef WLED_PIXELBUS_16PARALLEL - // 16-bit mode: mapping starts at DATA_OUT8_IDX for the wide 16-bit window - #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; - #else - sigIdx = I2S0O_DATA_OUT0_IDX; - #endif -#else - // 8-bit mode: mapping starts at DATA_OUT0_IDX (S2: DATA_OUT16_IDX for upper byte) - #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; - - //gpio_matrix_out(pin, sigIdx, inverted, false); - esp_rom_gpio_connect_out_signal(pin, sigIdx, inverted, false); // TODO: this is the new command in IDF V5, works in V4 too? + hwRoutePin(pin, idx, inverted); return idx; } @@ -453,7 +597,6 @@ void I2sBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ #ifdef WLED_PIXELBUS_16PARALLEL // 16-bit parallel encode: branchless gather + scatter, 64 bytes per source byte (16 channels) -// Returns the number of bytes actually written into dest (may be < destLen when data runs out). void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { for (size_t pos = 0; pos + 64 <= destLen; pos += 64) { // alwaysMask: channels with active data (HIGH step); bN: channels with bit N set @@ -480,10 +623,9 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t if (!alwaysMask) break; // no active channels produced data - // 16 x 32-bit stores, fully unrolled. uint32_t* p = (uint32_t*)(dest + pos); -#if defined(CONFIG_IDF_TARGET_ESP32S2) - // S2 layout: [step0, step1, step2, step3] (no half-word swap) +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + // S2/S3 layout: [step0, step1, step2, step3] (no half-word swap) // step0=HIGH, step1=data, step2=data, step3=LOW (or 0b1000, 0b1110) // 32-bit pair: p[0]=(bN<<16)|alwaysMask, p[1]=(0<<16)|bN #define EMIT(bN, OFF) \ @@ -529,21 +671,9 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t } if (!alwaysMask) break; - // 8-bit LCD mode with lcd_tx_wrx2_en=1 swaps bytes within 16-bit half-words: - // Memory [b0,b1,b2,b3] outputs as [b2,b3,b0,b1] (half-word swap within 32-bit word) - // Output cadence: [HIGH][data][data][LOW] -> steps [S0,S1,S2,S3] - // Memory write order for ESP32 (half-word swapped): [S2,S3,S0,S1] - // => as 32-bit: p[n] = (S0<<16)|S2 | upper, but since S3=LOW=0 and S1=data: - // => p[0] = (S0<<24)|(S2<<16)|(S1<<8)|S3 in byte view - // As single uint32_t (LE): byte0=S2=data, byte1=S3=0, byte2=S0=HIGH, byte3=S1=data - // = bN | 0 | alwaysMask | bN => as uint32: bN | (alwaysMask<<16) | ((uint32_t)bN<<24) - // - // For S2 (no half-word swap): layout [S0,S1,S2,S3] - // = alwaysMask | (bN<<8) | (bN<<16) | 0 - // As uint32: alwaysMask | ((uint32_t)bN<<8) | ((uint32_t)bN<<16) uint32_t* p = (uint32_t*)(dest + pos); -#if defined(CONFIG_IDF_TARGET_ESP32S2) - // S2: no swap, layout [S0,S1,S2,S3] = [HIGH,data,data,LOW] +#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + // S2/S3: no swap, layout [S0,S1,S2,S3] = [HIGH,data,data,LOW] #define EMIT8(bN, OFF) \ p[OFF] = (uint32_t)(alwaysMask) | ((uint32_t)(bN) << 8) | ((uint32_t)(bN) << 16); #else @@ -557,19 +687,18 @@ void IRAM_ATTR I2sBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t #undef EMIT8 } } - #endif // WLED_PIXELBUS_16PARALLEL void IRAM_ATTR I2sBusContext::fillBuffer(uint8_t bufIdx) { - // clear the buffer memset(_dmaBuffer[bufIdx], 0, _bufferSize); // clear the buffer, will be filled or left blank as reset signal (cant be skipped, some channels may have less data) if (_resetBytesLeft > 0) { - _dmaDesc[bufIdx]->length = _resetBytesLeft; - _dmaDesc[bufIdx]->qe.stqe_next = nullptr; // stop the engine after this one note: this assumes the reset period fits within one buffer, so don't set buffers too small i.e. at least 1k - _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers (is a multiple of 4 if set "naturally" below) + descSetLength(_dmaDesc[bufIdx], _resetBytesLeft); + descSetNext(_dmaDesc[bufIdx], nullptr); + _resetBytesLeft = WLEDPB_I2S_XFER_DONE_FLAG; // flag end of frame, don't queue any more buffers (is a multiple of 4 if set "naturally" below) return; // nothing to encode, this is a reset pulse, keep output low (all zeroes) } + uint32_t bytesToEncode = 0; uint8_t maxCh = 0; for (int ch = 0; ch < WLEDPB_I2S_MAX_CHANNELS; ch++) { @@ -594,12 +723,12 @@ void IRAM_ATTR I2sBusContext::fillBuffer(uint8_t bufIdx) { 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) - _dmaDesc[bufIdx]->length = _bufferSize; + descSetLength(_dmaDesc[bufIdx], _bufferSize); } else { - _dmaDesc[bufIdx]->length = newLen; // send the rest (zeroes) as a reset - _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers or it will mess up the DMA - _dmaDesc[bufIdx]->qe.stqe_next = nullptr; // reset fit into this buffer, end transfer after this is sent + 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 } } } @@ -624,80 +753,71 @@ bool I2sBusContext::startTransmit() { _resetBytesLeft = 0; - // allocate or reallocate DMA buffers if needed — deferred from init() so all channels - // can register first and _maxSrcBytes reflects the bus with the most pixels if (!_dmaAllocated) { if (!_allocDmaBuffers()) return false; } // Fill all buffers initially for (int i = 0; i < WLEDPB_I2S_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i]->qe.stqe_next = _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]; // restore circular buffer chain - _dmaDesc[i]->length = _bufferSize; + descSetNext(_dmaDesc[i], _dmaDesc[(i + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT]); // restore circular buffer chain + descSetLength(_dmaDesc[i], _bufferSize); fillBuffer(i); - _dmaDesc[i]->eof = 1; // enable eof, just in case - _dmaDesc[i]->owner = 1; // hand ownership over to DMA after descriptor init + descSetEof(_dmaDesc[i]); // enable eof, just in case + descSetOwnerDma(_dmaDesc[i]); // hand ownership over to DMA after descriptor init } _activeBuffer = 0; // start with first buffer - _remainingDataBuffers = WLEDPB_I2S_DMA_BUFFER_COUNT; _state = DriverState::Sending; - // Reset DMA and FIFO before starting - _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; - - // Clear and enable interrupts - _i2sDev->int_clr.val = 0xFFFFFFFF; - _i2sDev->int_ena.out_eof = 1; - - // Enable DMA and start - _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; + hwStartTransfer(); return true; } +//============================================================================== +// ISR / DMA callback +//============================================================================== + +#ifdef CONFIG_IDF_TARGET_ESP32S3 +IRAM_ATTR bool I2sBusContext::dmaCallback(gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data) { + (void)dma_chan; + (void)event_data; + I2sBusContext* ctx = (I2sBusContext*)user_data; + ctx->_processEof(); + return false; +} +#else void IRAM_ATTR I2sBusContext::dmaISR(void* arg) { I2sBusContext* ctx = (I2sBusContext*)arg; i2s_dev_t* dev = ctx->_i2sDev; - uint32_t status = dev->int_st.val; dev->int_clr.val = status; - if (!(status & I2S_OUT_EOF_INT_ST)) return; + ctx->_processEof(); +} +#endif - // The completed buffer just finished playing; DMA is now on the next buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer = (ctx->_activeBuffer + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; - if (ctx->_dmaDesc[completedBuf]->qe.stqe_next == nullptr) { // this was the last buffer - //ctx->_state = DriverState::WaitingReset; // if resetBytesLeft is not a multiple of 4 this flags end of transfer - dev->int_ena.out_eof = 0; // disable interrupt - dev->conf.tx_start = 0; - dev->out_link.start = 0; - ctx->_state = DriverState::Idle; - return; - } else if ((ctx->_resetBytesLeft != 3)) { // 3 means end of transfer (i.e. reset pulse) was encoded on last fillBuffer call - ctx->fillBuffer(completedBuf); // fill buffer, handle reset pulse and end of - ctx->_dmaDesc[completedBuf]->owner = 1; // owner is reset upon eof and must be handed back to DMA, do not hand it over if bus is stopped or it breaks the state-machine +void IRAM_ATTR I2sBusContext::_processEof() { + uint8_t completedBuf = _activeBuffer; + _activeBuffer = (_activeBuffer + 1) % WLEDPB_I2S_DMA_BUFFER_COUNT; + + if (descGetNext(_dmaDesc[completedBuf]) == nullptr) { + hwStopTransfer(); + _state = DriverState::Idle; return; } + + if (_resetBytesLeft != WLEDPB_I2S_XFER_DONE_FLAG) { // 3 means end of transfer (i.e. reset pulse) was encoded on last fillBuffer call + fillBuffer(completedBuf); // fill buffer, handle reset pulse and end of + //descSetOwnerDma(_dmaDesc[completedBuf]); // note: it looks like the owner is not reset upon eof, we do not need to hand it back + } } +// ============================================ // I2sBus implementation +// ============================================ I2sBus::I2sBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t busNum, uint8_t ledType, size_t numPixels) : _pin(pin) @@ -785,7 +905,7 @@ bool I2sBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannels) { return true; } -bool I2sBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { +bool I2sBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { if (!_initialized || !_ctx || !_encodeBuffer || _numPixels == 0) return false; // Wait for previous transmission to complete @@ -807,6 +927,5 @@ void I2sBus::setColorOrder(uint8_t co) { _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); } - } // namespace WLEDpixelBus #endif // WLEDPB_I2S_SUPPORT diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h index 1e12eea87b..326ffedbce 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_I2S.h @@ -1,12 +1,12 @@ /*------------------------------------------------------------------------- -WLEDpixelBus - parallel I2S output driver implementation +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 and ESP32 S2 +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 @@ -24,30 +24,41 @@ Each bus can have individual configuration of color channels but all must share namespace WLEDpixelBus { //============================================================================== -// I2S Parallel Bus - ESP32 and ESP32-S2 +// I2S Parallel Bus - ESP32, ESP32-S2, ESP32-S3 (LCD) //============================================================================== -#include "soc/i2s_struct.h" -#include "soc/i2s_reg.h" #include "driver/periph_ctrl.h" -#include "rom/lldesc.h" -#include "esp_intr_alloc.h" +#include "esp_rom_gpio.h" + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + #include "esp_private/gdma.h" + #include "hal/dma_types.h" + #include "hal/gpio_hal.h" + #include "hal/lcd_ll.h" + #include "soc/lcd_cam_struct.h" + #include "soc/gpio_sig_map.h" +#else + #include "soc/i2s_struct.h" + #include "soc/i2s_reg.h" + #include "rom/lldesc.h" + #include "esp_intr_alloc.h" +#endif // SOC_LCD_I80_BUSES: number of I2S peripherals that support the LCD Intel 8080 -// mode. 2 on ESP32 (I2S0 + I2S1), 1 on ESP32-S2 (I2S0 only). // TODO: support both buses on ESP32? (currently only bus 1 is used for LED output, one is for AR) +// mode. 2 on ESP32 (I2S0 + I2S1), 1 on ESP32-S2 (I2S0 only), 1 on ESP32-S3 (LCD_CAM). #define WLEDPB_I2S_BUS_COUNT SOC_LCD_I80_BUSES -// note: 4-step cadence with 16 parallel outs requires 8 bytes per source bit or 192bytes per LED, 1k buffer can hold ~5 LEDs, ISR will fire every 144us +// note: 4-step cadence with 16 parallel outs requires 8 bytes per source bit or 192bytes per RGB LED, 1k buffer can hold ~5 LEDs, ISR will fire every 144us // I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering is required for 16-parallel output. // TODO: this requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load // TODO2: the buffer count and size need to be checked agains memory usage fomulas, they may be incorrect #ifndef WLEDPB_I2S_DMA_BUFFER_COUNT #ifdef WLED_PIXELBUS_16PARALLEL - #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 + #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 // need 3 buffers in 16x parallel mode #else - #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT + #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 4 // supports 8 RMT (ESP32 only) #define WLEDPB_I2S_DMA_BUFFER_COUNT 3 #else #define WLEDPB_I2S_DMA_BUFFER_COUNT 2 // 2 buffers is enough if not constantly interrupted by RMT @@ -58,15 +69,14 @@ namespace WLEDpixelBus { // 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) + #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) + #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 peripheral for parallel output + * I2S bus context - manages shared I2S/LCD peripheral for parallel output * Uses circular DMA buffers with ISR-driven buffer refill */ class I2sBusContext { @@ -96,15 +106,57 @@ class I2sBusContext { void IRAM_ATTR fillBuffer(uint8_t bufIdx); bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); // Encoding (4-step cadence) + +#ifdef CONFIG_IDF_TARGET_ESP32S3 + static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); +#else static void IRAM_ATTR dmaISR(void* arg); +#endif + void IRAM_ATTR _processEof(); + + // Hardware abstraction + bool hwInit(const LedTiming& timing); + void hwDeinit(); + void hwStartTransfer(); + void IRAM_ATTR hwStopTransfer(); + void hwRoutePin(int8_t pin, int8_t idx, bool inverted); + + // DMA descriptor abstraction +#ifdef CONFIG_IDF_TARGET_ESP32S3 + using DmaDesc_t = dma_descriptor_t; + static inline void descSetBuf(DmaDesc_t* d, uint8_t* b) { d->buffer = b; } + static inline void descSetSizeAndLen(DmaDesc_t* d, size_t len) { d->dw0.size = len; d->dw0.length = len; } + static inline void descSetLength(DmaDesc_t* d, size_t len) { d->dw0.length = len; } + static inline void descSetNext(DmaDesc_t* d, DmaDesc_t* n) { d->next = n; } + static inline DmaDesc_t* descGetNext(DmaDesc_t* d) { return d->next; } + static inline void descSetEof(DmaDesc_t* d) { d->dw0.suc_eof = 1; } + static inline void descSetOwnerDma(DmaDesc_t* d) { d->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; } + static inline bool descIsOwnerDma(DmaDesc_t* d) { return d->dw0.owner == DMA_DESCRIPTOR_BUFFER_OWNER_DMA; } +#else + using DmaDesc_t = lldesc_t; + static inline void descSetBuf(DmaDesc_t* d, uint8_t* b) { d->buf = b; } + static inline void descSetSizeAndLen(DmaDesc_t* d, size_t len) { d->size = len; d->length = len; } + static inline void descSetLength(DmaDesc_t* d, size_t len) { d->length = len; } + static inline void descSetNext(DmaDesc_t* d, DmaDesc_t* n) { d->qe.stqe_next = n; } + static inline DmaDesc_t* descGetNext(DmaDesc_t* d) { return d->qe.stqe_next; } + static inline void descSetEof(DmaDesc_t* d) { d->eof = 1; } + static inline void descSetOwnerDma(DmaDesc_t* d) { d->owner = 1; } + static inline bool descIsOwnerDma(DmaDesc_t* d) { return d->owner == 1; } +#endif +#ifdef CONFIG_IDF_TARGET_ESP32S3 + gdma_channel_handle_t _dmaChannel; +#else uint8_t _busNum; i2s_dev_t* _i2sDev; + intr_handle_t _isrHandle; +#endif + volatile DriverState _state; bool _initialized; // DMA circular buffer chain - lldesc_t* _dmaDesc[WLEDPB_I2S_DMA_BUFFER_COUNT]; + DmaDesc_t* _dmaDesc[WLEDPB_I2S_DMA_BUFFER_COUNT]; uint8_t* _dmaBuffer[WLEDPB_I2S_DMA_BUFFER_COUNT]; size_t _bufferSize; // actual allocated DMA buffer size (per buffer) size_t _maxSrcBytes; // max source (encoded pixel) bytes across all registered channels; drives DMA sizing @@ -117,9 +169,6 @@ class I2sBusContext { LedTiming _timing; uint32_t _clockDiv; - // ISR handle - intr_handle_t _isrHandle; - // Channel data struct ChannelData { I2sBus* bus; @@ -151,7 +200,7 @@ class I2sBus : public PixelBus { * @param timing LED timing * @param colorOrder Color order * @param numChannels Bytes per pixel - * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2) + * @param busNum I2S bus number (0 or 1 on ESP32, 0 on S2/S3) * @param ledType LED chip type constant * @param numPixels Number of pixels; stored for DMA buffer sizing in I2sBusContext */ @@ -161,8 +210,7 @@ class I2sBus : public PixelBus { bool begin() override; void end() override; - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; + bool show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct = nullptr) override; bool canShow() const override; #ifdef WLED_DEBUG_BUS const char* getTypeStr() const override { return "I2S"; } diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp deleted file mode 100644 index 432d447dd1..0000000000 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.cpp +++ /dev/null @@ -1,640 +0,0 @@ -/*------------------------------------------------------------------------- - -WLEDpixelBus - parallel 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. - -LCD hardware is available on ESP32 S3 only -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.h" -#ifdef WLEDPB_LCD_SUPPORT -#include "WLEDpixelBus_LCD.h" - -namespace WLEDpixelBus { - -//============================================================================== -// LCD Bus Implementation (ESP32-S3) -//============================================================================== -/*------------------------------------------------------------------------- -WLEDpixelBus - LCD Implementation with Continuous DMA (Glitch-Free) - -Key design: -- DMA runs continuously in circular mode (buf0 -> buf1 -> buf0 -> ...) -- Buffers are filled with data or zeros (for reset period) -- Stop only after reset period completes on buffer boundary -- No DMA reconfiguration during transmission -- DMA buffer size is computed from the largest registered bus and allocated - once in startTransmit() (deferred allocation). --------------------------------------------------------------------------*/ - -#include "driver/periph_ctrl.h" -#include "esp_private/gdma.h" -#include "esp_rom_gpio.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" - -// ============================================ -// Configuration -// ============================================ - -#define LCD_LOG(x...) // Serial.printf(x) // define to debug - -LcdBusContext* LcdBusContext::_instance = nullptr; -uint8_t LcdBusContext::_refCount = 0; - -LcdBusContext* LcdBusContext::get() { - if (_instance == nullptr) { - _instance = new LcdBusContext(); - } - _refCount++; - return _instance; -} - -void LcdBusContext::release() { - if (_refCount == 0) return; - _refCount--; - if (_refCount == 0 && _instance) { - delete _instance; - _instance = nullptr; - } -} - -LcdBusContext::LcdBusContext() - : _state(DriverState::Idle) - , _initialized(false) - , _use16Bit(false) - , _dmaChannel(nullptr) - , _timing{0, 0, 0, 0, 0} - , _bufferSize(0) - , _channelCount(0) - , _channelMask(0) - , _stagedMask(0) - , _maxDataLen(0) - , _activeBuffer(0) - , _maxSrcBytes(0) // HEADER - , _resetBytesLeft(0) // HEADER - , _dmaAllocated(false) // HEADER -{ - for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - _channels[i] = {nullptr, -1, nullptr, 0, 0, false}; - } - for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i] = nullptr; - _dmaBuffer[i] = nullptr; - } -} - -LcdBusContext::~LcdBusContext() { - deinit(); -} - -bool LcdBusContext::init(const LedTiming& timing, bool use16Bit) { - if (_initialized) return true; - - LCD_LOG("Init: deferred alloc, cadence=%d, T0H=%u T1H=%u bitPeriod=%u", - WLEDPB_LCD_CADENCE_STEPS, timing.t0h_ns, timing.t1h_ns, timing.bitPeriod()); - - _timing = timing; - _use16Bit = use16Bit; - - uint32_t bitPeriodNs = timing.bitPeriod(); - - // Enable LCD_CAM peripheral - periph_module_enable(PERIPH_LCD_CAM_MODULE); - periph_module_reset(PERIPH_LCD_CAM_MODULE); - - // Reset LCD - LCD_CAM.lcd_user.lcd_reset = 1; - esp_rom_delay_us(100); - LCD_CAM.lcd_user.lcd_reset = 0; - esp_rom_delay_us(100); - - // Calculate clock divider - double clkm_div = (double)bitPeriodNs / WLEDPB_LCD_CADENCE_STEPS / 1000.0 * 240.0; - - LCD_LOG(" Bit period: %u ns, clock div: %.2f", bitPeriodNs, clkm_div); - - if (clkm_div > LCD_LL_CLK_FRAC_DIV_N_MAX || clkm_div < 2.0) { - LCD_LOG("ERROR: Invalid clock divider"); - deinit(); - 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; // 16-bit output for up to 16 channels -#else - LCD_CAM.lcd_user.lcd_2byte_en = 0; // 8-bit output for up to 8 channels -#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) { - LCD_LOG("ERROR: GDMA alloc failed: %d", err); - deinit(); - return false; - } - - err = gdma_connect(_dmaChannel, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); - if (err != ESP_OK) { - LCD_LOG("ERROR: GDMA connect failed: %d", err); - deinit(); - return false; - } - - gdma_strategy_config_t strategy_config = { - .owner_check = false, - .auto_update_desc = false - }; - gdma_apply_strategy(_dmaChannel, &strategy_config); - - // Register DMA callback - gdma_tx_event_callbacks_t tx_cbs = {.on_trans_eof = dmaCallback}; - gdma_register_tx_event_callbacks(_dmaChannel, &tx_cbs, this); - - _initialized = true; - LCD_LOG("Init OK: clkm_div=%u+%u/%u", - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_num, - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_b, - (unsigned)LCD_CAM.lcd_clock.lcd_clkm_div_a); - return true; -} - -void LcdBusContext::deinit() { - // Stop transmission - LCD_CAM.lcd_user.lcd_start = 0; - _state = DriverState::Idle; - - if (_dmaChannel) { - gdma_stop(_dmaChannel); - gdma_disconnect(_dmaChannel); - gdma_del_channel(_dmaChannel); - _dmaChannel = nullptr; - } - - for (int i = 0; i < WLEDPB_LCD_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; - } - } - - if (_initialized) { - periph_module_disable(PERIPH_LCD_CAM_MODULE); - } - - _dmaAllocated = false; - _initialized = false; -} - -// Compute the DMA buffer size from _maxSrcBytes (set during registerChannel() by the largest bus), -// then allocate the circular DMA descriptor/buffer chain. -// Called once from startTransmit() after all channels have registered. -bool LcdBusContext::_allocDmaBuffers() { - if (_dmaBuffer[0] != nullptr) return true; // already allocated - - size_t dmaBytesPerSrc = _use16Bit ? 64 : 32; - _bufferSize = (dmaBytesPerSrc * _maxSrcBytes) / WLEDPB_LCD_DMA_BUFFER_COUNT; - _bufferSize = (_bufferSize + 3) & ~3; // align to 4 bytes - if (_bufferSize > WLEDPB_LCD_DMA_BUFFER_SIZE) _bufferSize = WLEDPB_LCD_DMA_BUFFER_SIZE; - if (_bufferSize < 1024) _bufferSize = 1024; // MIN_DMA_BUFFER_SIZE - - for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { - _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, _bufferSize, MALLOC_CAP_DMA); - if (!_dmaBuffer[i]) { - LCD_LOG("ERROR: DMA buffer %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaBuffer[i], 0, _bufferSize); - - _dmaDesc[i] = (dma_descriptor_t*)heap_caps_malloc(sizeof(dma_descriptor_t), MALLOC_CAP_DMA); - if (!_dmaDesc[i]) { - LCD_LOG("ERROR: DMA desc %d alloc failed", i); - deinit(); - return false; - } - memset(_dmaDesc[i], 0, sizeof(dma_descriptor_t)); - } - - // Setup DMA descriptors in CIRCULAR mode (restored per-transmission in startTransmit) - for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i]->dw0.size = _bufferSize; - _dmaDesc[i]->dw0.length = _bufferSize; - _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; - _dmaDesc[i]->dw0.suc_eof = 1; // Always trigger EOF for ISR - _dmaDesc[i]->buffer = _dmaBuffer[i]; - _dmaDesc[i]->next = _dmaDesc[(i + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT]; // circular chain - } - - _dmaAllocated = true; - LCD_LOG("DMA buffers allocated: bufSize=%u x%u", (unsigned)_bufferSize, WLEDPB_LCD_DMA_BUFFER_COUNT); - return true; -} - -int8_t LcdBusContext::registerChannel(int8_t pin, LcdBus* bus, size_t srcBytes, bool inverted) { - int8_t idx = -1; - for (int i = 0; i < WLEDPB_LCD_MAX_CHANNELS; i++) { - if (!_channels[i].active) { - idx = i; - break; - } - } - if (idx < 0) return -1; - - _channels[idx].bus = bus; - _channels[idx].pin = pin; - _channels[idx].active = true; - _channelCount++; - _channelMask |= (1 << idx); - - if (srcBytes > _maxSrcBytes) _maxSrcBytes = srcBytes; - - esp_rom_gpio_connect_out_signal(pin, LCD_DATA_OUT0_IDX + idx, inverted, false); - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO); - gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_3); - - LCD_LOG("Channel %d: pin=%d, mask=0x%04X, srcBytes=%u", idx, pin, _channelMask, (unsigned)srcBytes); - return idx; -} - -void LcdBusContext::unregisterChannel(int8_t channelIdx) { - if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; - if (!_channels[channelIdx].active) return; - - if (_channels[channelIdx].pin >= 0) { - gpio_reset_pin((gpio_num_t)_channels[channelIdx].pin); - } - - _channels[channelIdx] = {nullptr, -1, nullptr, 0, 0, false}; - _channelCount--; - _channelMask &= ~(1 << channelIdx); -} - -void LcdBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_t len) { - if (channelIdx < 0 || channelIdx >= WLEDPB_LCD_MAX_CHANNELS) return; - - _channels[channelIdx].srcData = data; - _channels[channelIdx].srcLen = len; - _channels[channelIdx].srcPos = 0; - - if (len > _maxDataLen) _maxDataLen = len; - - if (_stagedMask & (1 << channelIdx)) _stagedMask = 0; - _stagedMask |= (1 << channelIdx); -} - -#ifdef WLED_PIXELBUS_16PARALLEL -// 16-bit parallel encode: branchless gather + scatter, 64 bytes per source byte (16 channels) -void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { - 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 (int ch = 0; ch < maxChannel; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - const uint16_t m = (uint16_t)(1u << ch); - alwaysMask |= m; - const uint8_t b = _channels[ch].srcData[_channels[ch].srcPos++]; - 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; - - // S3 LCD: no byte swapping. - uint32_t* p = (uint32_t*)(dest + pos); - #define EMIT(bN, OFF) \ - p[OFF] = ((uint32_t)(bN) << 16) | alwaysMask; \ - p[OFF+1] = (uint32_t)(bN); - EMIT(b0, 0) EMIT(b1, 2) EMIT(b2, 4) EMIT(b3, 6) - EMIT(b4, 8) EMIT(b5, 10) EMIT(b6, 12) EMIT(b7, 14) - #undef EMIT - } -} - -#else // WLED_PIXELBUS_16PARALLEL - -// 8-bit parallel encode: branchless gather + scatter, 32 bytes per source byte (8 channels) -void IRAM_ATTR LcdBusContext::encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel) { - for (size_t pos = 0; pos + 32 <= destLen; pos += 32) { - uint8_t alwaysMask = 0; - uint8_t b0 = 0, b1 = 0, b2 = 0, b3 = 0; - uint8_t b4 = 0, b5 = 0, b6 = 0, b7 = 0; - - for (int ch = 0; ch < maxChannel; ch++) { - if (!_channels[ch].active) continue; - if (_channels[ch].srcPos >= _channels[ch].srcLen) continue; - const uint8_t m = (uint8_t)(1u << ch); - alwaysMask |= m; - const uint8_t b = _channels[ch].srcData[_channels[ch].srcPos++]; - b0 |= m & (uint8_t)(0u - ((b >> 7) & 1u)); - b1 |= m & (uint8_t)(0u - ((b >> 6) & 1u)); - b2 |= m & (uint8_t)(0u - ((b >> 5) & 1u)); - b3 |= m & (uint8_t)(0u - ((b >> 4) & 1u)); - b4 |= m & (uint8_t)(0u - ((b >> 3) & 1u)); - b5 |= m & (uint8_t)(0u - ((b >> 2) & 1u)); - b6 |= m & (uint8_t)(0u - ((b >> 1) & 1u)); - b7 |= m & (uint8_t)(0u - ((b >> 0) & 1u)); - } - if (!alwaysMask) break; - - uint32_t* p = (uint32_t*)(dest + pos); - #define EMIT8(bN, OFF) \ - p[OFF] = (uint32_t)(alwaysMask) | ((uint32_t)(bN) << 8) | ((uint32_t)(bN) << 16); - EMIT8(b0, 0) EMIT8(b1, 1) EMIT8(b2, 2) EMIT8(b3, 3) - EMIT8(b4, 4) EMIT8(b5, 5) EMIT8(b6, 6) EMIT8(b7, 7) - #undef EMIT8 - } -} - -#endif // WLED_PIXELBUS_16PARALLEL - -void IRAM_ATTR LcdBusContext::fillBuffer(uint8_t bufIdx) { - memset(_dmaBuffer[bufIdx], 0, _bufferSize); - - if (_resetBytesLeft > 0) { - _dmaDesc[bufIdx]->dw0.length = _resetBytesLeft; - _dmaDesc[bufIdx]->next = nullptr; // stop the engine after this one - _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers - return; - } - - uint32_t bytesToEncode = 0; - uint8_t maxCh = 0; - for (int ch = 0; ch < WLEDPB_LCD_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 dmaBytesPerSrc = _use16Bit ? 64 : 32; - uint32_t translatedbytes = bytesToEncode * dmaBytesPerSrc; - translatedbytes = translatedbytes > _bufferSize ? _bufferSize : translatedbytes; - encode4Step(_dmaBuffer[bufIdx], translatedbytes, maxCh); - - if (translatedbytes < _bufferSize) { - // Data ran out before the buffer was full (i.e. we are done), compute the minimum reset period we must send as zero cycles - uint32_t resetNs = _timing.reset_us * 1000; - uint32_t bitPeriodNs = _timing.bitPeriod() + 1; // +1 to ensure no division by zero and slightly over-estimate the reset cycle - uint32_t zeroCycles = resetNs / bitPeriodNs; - size_t resetBytes = zeroCycles * (dmaBytesPerSrc / 8); // one cycle is 4 clocks, on each clock one/two buffer byte(s) sent out in parallel - - size_t newLen = translatedbytes + resetBytes; - if (newLen > _bufferSize) { - _resetBytesLeft = newLen - _bufferSize; // reset pulse does not fit into this buffer frame, send another one - _dmaDesc[bufIdx]->dw0.length = _bufferSize; - } else { - _dmaDesc[bufIdx]->dw0.length = newLen; // send the rest (zeroes) as a reset - _resetBytesLeft = 3; // flag end of frame, don't queue any more buffers or it will mess up the DMA - _dmaDesc[bufIdx]->next = nullptr; // reset fit into this buffer, end transfer after this is sent - } - } -} - -bool LcdBusContext::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_LCD_MAX_CHANNELS; ch++) { - if (_channels[ch].active) { - _channels[ch].srcPos = 0; - if (_channels[ch].srcLen > _maxDataLen) { - _maxDataLen = _channels[ch].srcLen; - } - } - } - if (_maxDataLen == 0) return false; - - _resetBytesLeft = 0; - - // allocate or reallocate DMA buffers if needed — deferred from init() so all channels - // can register first and _maxSrcBytes reflects the bus with the most pixels - if (!_dmaAllocated) { - if (!_allocDmaBuffers()) return false; - } - - // Fill all buffers initially and restore circular chain - for (int i = 0; i < WLEDPB_LCD_DMA_BUFFER_COUNT; i++) { - _dmaDesc[i]->next = _dmaDesc[(i + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT]; // restore circular buffer chain - _dmaDesc[i]->dw0.length = _bufferSize; - fillBuffer(i); - _dmaDesc[i]->dw0.suc_eof = 1; // enable eof, just in case - _dmaDesc[i]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; // hand ownership over to DMA after descriptor init - } - - _activeBuffer = 0; // start with first buffer - _state = DriverState::Sending; - - // Start DMA (circular mode - descriptors already linked) - 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; - - return true; -} - -IRAM_ATTR bool LcdBusContext::dmaCallback(gdma_channel_handle_t dma_chan, - gdma_event_data_t* event_data, - void* user_data) { - LcdBusContext* ctx = (LcdBusContext*)user_data; - - // The completed buffer just finished playing; DMA is now on the next buffer - uint8_t completedBuf = ctx->_activeBuffer; - ctx->_activeBuffer = (ctx->_activeBuffer + 1) % WLEDPB_LCD_DMA_BUFFER_COUNT; - - if (ctx->_dmaDesc[completedBuf]->next == nullptr) { // this was the last buffer - LCD_CAM.lcd_user.lcd_start = 0; - gdma_stop(ctx->_dmaChannel); - ctx->_state = DriverState::Idle; - return false; - } - - if (ctx->_resetBytesLeft != 3) { - ctx->fillBuffer(completedBuf); // fill buffer, handle reset pulse and end of frame - ctx->_dmaDesc[completedBuf]->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; // owner is reset upon eof and must be handed back to DMA - } - - return false; // Do not yield OS for this DMA streaming interrupt -} - -// ============================================ -// LcdBus Implementation -// ============================================ - -LcdBus::LcdBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, size_t numPixels, bool use16Bit, uint8_t ledType) - : _pin(pin) - , _use16Bit(use16Bit) - , _timing(timing) - , _initialized(false) - , _channelIdx(-1) - , _ctx(nullptr) -{ - _encoder = ColorEncoder(colorOrder, numChannels, ledType); - _ledType = ledType; -} - -LcdBus::~LcdBus() { - end(); -} - -bool LcdBus::begin() { - if (_initialized) return true; - - _ctx = LcdBusContext::get(); - if (!_ctx) return false; - - if (!_ctx->init(_timing, _use16Bit)) { - LcdBusContext::release(); - _ctx = nullptr; - return false; - } - - // pass our encoded byte count so the context can size DMA buffers for the largest bus - const size_t srcBytes = (size_t)_numPixels * _encoder.getPixelBytes(); - _channelIdx = _ctx->registerChannel(_pin, this, srcBytes, _inverted); - if (_channelIdx < 0) { - LcdBusContext::release(); - _ctx = nullptr; - return false; - } - - _initialized = true; - if (!allocateEncodeBuffer(_numPixels, _encoder.getPixelBytes())) { - end(); - return false; - } - LCD_LOG("LcdBus: ch=%d pin=%d", _channelIdx, _pin); - return true; -} - -// invert output signal, must be set before begin() -void LcdBus::setInverted(bool inv) { - _inverted = inv; -} - -void LcdBus::end() { - if (!_initialized) return; - - if (_ctx) { - // Wait for any active transmission to complete before cleanup - while (!_ctx->isIdle()) vTaskDelay(1); - _ctx->unregisterChannel(_channelIdx); - LcdBusContext::release(); - _ctx = nullptr; - } - - if (_encodeBuffer) { - heap_caps_free(_encodeBuffer); - _encodeBuffer = nullptr; - _encodeBufferSize = 0; - } - - _initialized = false; -} - -bool LcdBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { - if (!_initialized || !_ctx || !_encodeBuffer || _numPixels == 0) return false; - - // Wait for previous transmission to complete - uint32_t start = millis(); - while (!_ctx->isIdle()) { - if (millis() - start > 1000) { - _ctx->forceIdle(); - break; - } - yield(); - } - - // Send already-encoded buffer directly - _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodeBufferSize); - return _ctx->startTransmit(); -} - -bool LcdBus::canShow() const { - if (!_ctx) return true; - return _ctx->isIdle(); -} - -void LcdBus::setColorOrder(uint8_t co) { - _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); -} - -} // namespace WLEDpixelBus -#endif \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h deleted file mode 100644 index ab94485069..0000000000 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_LCD.h +++ /dev/null @@ -1,177 +0,0 @@ -/*------------------------------------------------------------------------- - -WLEDpixelBus - parallel 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. - -LCD hardware is available on ESP32 S3 only -Default is 8 parallel outputs and double DMA buffering but it also supports 16 parallel outputs if needed -For 16 parallel output, triple buffering is required for glitch-free output. -Data is output in 4-step cadence meaning each LED bit is encoded into 4 I2S bits. '0' is 0b1000 and '1' is 0b1110 -Encoding is highly optimized for speed as encoding is done "on the fly" while the other buffer is being sent out using DMA. -The RAM usage of the sendout buffer is number of LEDs * bytes per LED + DMA buffer size -3k per DMA buffer works well, enough for 32 RGB LEDs in 8x parallel output or roughly 0.9ms between buffer swaps -Each bus can have individual configuration of color channels but all must share the same timing - --------------------------------------------------------------------------*/ - -#pragma once -#ifdef WLEDPB_LCD_SUPPORT -#include "WLEDpixelBus.h" - -namespace WLEDpixelBus { - -//============================================================================== -// LCD Parallel Bus - ESP32-S3 only -//============================================================================== - -#include "driver/periph_ctrl.h" -#include "esp_private/gdma.h" -#include "esp_rom_gpio.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" - -#ifndef WLEDPB_LCD_DMA_BUFFER_SIZE -#define WLEDPB_LCD_DMA_BUFFER_SIZE (3*1024) // 2048 works as well, still no glitches with 512 and an RMT running as well, 1024 seems to work fine, needs more testing (larger buffer might improve FPS) -#endif - -// I2S DMA buffer count for circular linked list. For 8-parallel output, double buffering is enough, tripple buffering may be required for 16-parallel output -// TODO: this requires more stress-testing to ensure glitch-free outputs, for 16-parallel maybe even 4 buffers are needed under heavy load -// TODO2: the buffer count and size need to be checked agains memory usage fomulas, they may be incorrect -#ifndef WLEDPB_LCD_DMA_BUFFER_COUNT - #ifdef WLED_PIXELBUS_16PARALLEL - #define WLEDPB_LCD_DMA_BUFFER_COUNT 3 - #else - #define WLEDPB_LCD_DMA_BUFFER_COUNT 2 - #endif -#endif - -#ifndef WLEDPB_LCD_CADENCE_STEPS -#define WLEDPB_LCD_CADENCE_STEPS 4 // TODO: 3-step cadence was mostly abandoned, fully remove it? 4 step cadence is generally better to hit timing targets. -#endif - -// 16-bit parallel mode supports 16 channels; 8-bit supports 8 channels. -#ifdef WLED_PIXELBUS_16PARALLEL - #define WLEDPB_LCD_MAX_CHANNELS 16 -#else - #define WLEDPB_LCD_MAX_CHANNELS 8 -#endif - - -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE >= 64, "DMA buffer too small"); -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE <= 4092, "DMA buffer too large"); -static_assert(WLEDPB_LCD_DMA_BUFFER_SIZE % 4 == 0, "DMA buffer must be multiple of 4"); -static_assert(WLEDPB_LCD_CADENCE_STEPS == 3 || WLEDPB_LCD_CADENCE_STEPS == 4, "Cadence must be 3 or 4"); -// SOC_LCD_I80_BUS_WIDTH is the physical output width of the LCD I80 peripheral. -// On ESP32-S3 this is 16, so 16-parallel is the hardware maximum. -static_assert(WLEDPB_LCD_MAX_CHANNELS <= SOC_LCD_I80_BUS_WIDTH, - "WLEDPB_LCD_MAX_CHANNELS exceeds hardware LCD bus width (SOC_LCD_I80_BUS_WIDTH)"); - -class LcdBusContext { -public: - static LcdBusContext* get(); - static void release(); - - bool init(const LedTiming& timing, bool use16Bit = false); - void deinit(); - - int8_t registerChannel(int8_t pin, LcdBus* bus, size_t srcBytes, bool inverted = false); - void unregisterChannel(int8_t channelIdx); - uint8_t getChannelCount() const { return _channelCount; } - - bool startTransmit(); - bool isIdle() const { return _state == DriverState::Idle; } - - void forceIdle() { - LCD_CAM.lcd_user.lcd_start = 0; - if (_dmaChannel) gdma_stop(_dmaChannel); - _state = DriverState::Idle; - } - - void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); - -private: - LcdBusContext(); - ~LcdBusContext(); - - void IRAM_ATTR encode4Step(uint8_t* dest, size_t destLen, uint8_t maxChannel); - void fillBuffer(uint8_t bufIdx); - bool _allocDmaBuffers(); // allocate/reallocate DMA buffers sized for the largest registered channel - static IRAM_ATTR bool dmaCallback(gdma_channel_handle_t dma_chan, gdma_event_data_t* event_data, void* user_data); - - volatile DriverState _state; - bool _initialized; - bool _use16Bit; - - // DMA (circular linked list - never modified during operation) - gdma_channel_handle_t _dmaChannel; - dma_descriptor_t* _dmaDesc[WLEDPB_LCD_DMA_BUFFER_COUNT]; - uint8_t* _dmaBuffer[WLEDPB_LCD_DMA_BUFFER_COUNT]; - - // Timing - LedTiming _timing; - size_t _bufferSize; - size_t _maxSrcBytes; // max source (encoded pixel) bytes across all registered channels; drives DMA sizing - bool _dmaAllocated; // true when DMA buffers are allocated and reflect current _maxSrcBytes - volatile uint8_t _activeBuffer; - volatile uint8_t _remainingDataBuffers; - volatile uint16_t _resetBytesLeft; - - // Channels - struct ChannelData { - LcdBus* bus; - int8_t pin; - const uint8_t* srcData; - size_t srcLen; - size_t srcPos; - bool active; - }; - ChannelData _channels[WLEDPB_LCD_MAX_CHANNELS]; - uint8_t _channelCount; - uint16_t _channelMask; - uint16_t _stagedMask; - size_t _maxDataLen; - - - static LcdBusContext* _instance; - static uint8_t _refCount; - friend class LcdBus; -}; - -class LcdBus : public PixelBus { -public: - LcdBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, size_t numPixels, bool use16Bit = false, uint8_t ledType = 0); - ~LcdBus() override; - - bool begin() override; - void end() override; - - bool show(const uint32_t* pixels, uint16_t numPixels, - const CctPixel* cct = nullptr) override; - bool canShow() const override; -#ifdef WLED_DEBUG_BUS - const char* getTypeStr() const override { return "LCD"; } -#endif - - void setInverted(bool inv) override; - void setColorOrder(uint8_t co); - -private: - int8_t _pin; - bool _use16Bit; - LedTiming _timing; - bool _inverted = false; - bool _initialized; - - int8_t _channelIdx; - LcdBusContext* _ctx; -}; - -} // namespace WLEDpixelBus - -#endif // WLEDPB_LCD_SUPPORT From 687169b2ff153772d5332d28db53a6be48a23cb9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jul 2026 02:04:47 +0200 Subject: [PATCH 168/173] bugfix in RMT driver (IRAM not DRAM), add TODOs, cleanup --- wled00/src/WLEDpixelBus/WLEDpixelBus.h | 13 +----- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 43 ++++++++----------- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h | 5 +-- .../src/WLEDpixelBus/WLEDpixelBus_Timings.h | 2 +- 4 files changed, 24 insertions(+), 39 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.h b/wled00/src/WLEDpixelBus/WLEDpixelBus.h index ebcb20a32e..70296bd618 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.h @@ -298,8 +298,6 @@ class PixelBus { public: virtual ~PixelBus() { - // Subclass end() should free _encodeBuffer with the correct allocator and - // set it to nullptr before the base destructor runs. This is a safety net. if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; } } @@ -384,15 +382,8 @@ class PixelBus { virtual bool begin() = 0; virtual void end() = 0; - - /** - * Show pixels — sends the pre-encoded _encodeBuffer to hardware. - * The pixel/numPixels/cct parameters are ignored; encoding is done - * per-pixel by setPixel() before show() is called. - */ - virtual bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, - const CctPixel* cct = nullptr) = 0; - + // show() sends the pre-encoded _encodeBuffer to hardware. + virtual bool show(const uint32_t* pixels = nullptr, uint16_t numPixels = 0, const CctPixel* cct = nullptr) = 0; // TODO: remove the parameters, they are unused virtual bool canShow() const = 0; #ifdef WLED_DEBUG_BUS virtual const char* getTypeStr() const = 0; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index f643eb5049..356c14159e 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -22,8 +22,8 @@ namespace WLEDpixelBus { // RMT Bus Implementation //============================================================================== -// Per-channel context table - stored in DRAM for ISR access -DRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[WPB_RMT_CHANNELS] = {}; +// Per-channel context table - stored in IRAM_ATTR for ISR access +IRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[WPB_RMT_CHANNELS] = {}; // Explicit IRAM tranlator callback wrappers for each channel (ensures the function is placed in IRAM which is dropped when using templates) void IRAM_ATTR RmtBus::translator_ch0(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(0, s, d, ss, w, ts, in); } @@ -39,7 +39,7 @@ void IRAM_ATTR RmtBus::translator_ch6(const void* s, rmt_item32_t* d, size_t ss, void IRAM_ATTR RmtBus::translator_ch7(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(7, s, d, ss, w, ts, in); } #endif // Jump table stored in DRAM so ISR code can quickly find the correct wrapper -DRAM_ATTR const sample_to_rmt_t RmtBus::s_callbacks[WPB_RMT_CHANNELS] = { +IRAM_ATTR const sample_to_rmt_t RmtBus::s_callbacks[WPB_RMT_CHANNELS] = { RmtBus::translator_ch0, RmtBus::translator_ch1 #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 ,RmtBus::translator_ch2, RmtBus::translator_ch3 @@ -96,7 +96,6 @@ void RmtBus::updateRmtTiming() { bool RmtBus::begin() { if (_initialized) return true; - uint8_t blocksToUse = 1; uint8_t maxTxChannels = getRmtMaxChannels(); @@ -132,13 +131,13 @@ bool RmtBus::begin() { _channel = s_currentChannelIndex; blocksToUse = k; } + #ifdef RMT_USE_SINGLE_MEM_BLOCK + blocksToUse = 1; + #endif s_currentChannelIndex += blocksToUse; s_usedBlocks += blocksToUse; s_allocatedCount++; -#ifdef RMT_USE_SINGLE_MEM_BLOCK - blocksToUse = 1; -#endif if (_channel >= (int8_t)maxTxChannels) { //DEBUG_PRINTF_P(PSTR("[WPB] RMT channel %d >= max %u, FAIL\n"), _channel, maxTxChannels); @@ -197,7 +196,7 @@ bool RmtBus::begin() { if (!_usingRmtHi) { // Fallback to IDF rmt driver + translator - err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3)); // note: rmt+parallelspi even deadlocks at level1 + err = rmt_driver_install(_rmtChannel, 0, (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LOWMED)); if (err != ESP_OK) { return false; } @@ -236,28 +235,24 @@ void RmtBus::end() { // Free encode buffer with the same allocator used in allocateEncodeBuffer() (plain malloc) if (_encodeBuffer) { free(_encodeBuffer); _encodeBuffer = nullptr; _encodeBufferSize = 0; } - _initialized = false; _usingRmtHi = false; } -bool RmtBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { +bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cct) { // Encoding is done per-pixel in setPixel(); _encodeBuffer is ready to ship. if (!_initialized || !_encodeBuffer || _numPixels == 0) return false; const size_t dataLen = _encodeBufferSize; - + esp_err_t err; if (_usingRmtHi) { - // Write() waits for any in-flight transfer internally before starting the new one - return RmtHiDriver::Write(_rmtChannel, _encodeBuffer, dataLen) == ESP_OK; + err = (RmtHiDriver::Write(_rmtChannel, _encodeBuffer, dataLen) == ESP_OK); + } else { + err = rmt_wait_tx_done(_rmtChannel, 1000 / portTICK_PERIOD_MS); // wait 1s max + if (err == ESP_OK) + err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); } - - // Wait for previous transmission on THIS channel to complete (IDF driver) - rmt_wait_tx_done((rmt_channel_t)_rmtChannel, portMAX_DELAY); - - esp_err_t err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); - - return err == ESP_OK; + return err; } bool RmtBus::canShow() const { @@ -272,13 +267,13 @@ void RmtBus::setColorOrder(uint8_t co) { //note: using O2 optimization has little to no effect on FPS void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_item32_t* dest, size_t src_size, size_t wanted_num, size_t* translated_size, size_t* item_num) { -/* + // safety check - should never happen if (src == nullptr || dest == nullptr) { *translated_size = 0; *item_num = 0; return; - }*/ + } const uint8_t* psrc = (const uint8_t*)src; @@ -308,8 +303,8 @@ void IRAM_ATTR RmtBus::translateInternal(uint8_t channel, const void* src, rmt_i } // If max_bytes == src_size, it means we've reached the end of the LED strip. - if (bytes_to_process == src_size) { - dest[(bytes_to_process * 8) - 1].duration1 = s_contexts[channel].resetDuration; + if (bytes_to_process > 0 && bytes_to_process == src_size) { + dest[(bytes_to_process * 8) - 1].duration1 = s_contexts[channel].resetDuration; // set the last (low) pulse to reset duration } // *translated_size = bytes_to_process; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h index 3842f2f28d..8778e6aafb 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.h @@ -75,11 +75,10 @@ class RmtBus : public PixelBus { bool _initialized; LedTiming _timing; rmt_channel_t _rmtChannel; - bool _usingRmtHi; + bool _usingRmtHi; // TODO: use #ifdef only and remove this variable // _encodeBuffer and _encodeBufferSize are in PixelBus base - - static uint8_t s_expectedChannels; + static uint8_t s_expectedChannels; // TODO: make none static static uint8_t s_allocatedCount; static uint8_t s_currentChannelIndex; static uint8_t s_usedBlocks; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h index 806ecd6994..adf2461e19 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Timings.h @@ -24,7 +24,7 @@ struct LedTiming { 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 + uint32_t reset_us; // Reset/latch time in microseconds // TODO: globally limit this? RMT can do ~800us max (32767 ticks of 25ns) constexpr LedTiming(uint16_t t0h, uint16_t t0l, uint16_t t1h, uint16_t t1l, uint32_t reset) : t0h_ns(t0h), t0l_ns(t0l), t1h_ns(t1h), t1l_ns(t1l), reset_us(reset) {} From a531320283f25cfcc9eb3a5565730933d8a1b8c2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jul 2026 02:28:48 +0200 Subject: [PATCH 169/173] use max current for SM16825 as it always was --- wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h index 9951a1ecb8..c3d9420913 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_Features.h @@ -16,8 +16,8 @@ prefix data (TM1914), suffix data (SM16825) and brigthness to LED hardware curre // bits 31..7 (25 bits): current gain OUT R,G,B,W,Y — 5 bits each, 0x1F = 310mA, 0x00 = 10.2mA, step ~10.1mA // bits 6..5 ( 2 bits): standby enable — 0b00 = normal op (0b10 = standby) // bits 4..0 ( 5 bits): reserved — all 1 recommended -// static constexpr uint8_t SM16825_SUFFIX[4] = { 0xFF, 0xFF, 0xFF, 0x9F }; // set max current (not really safe) -static constexpr uint8_t SM16825_SUFFIX[4] = { 0x08, 0x42, 0x10, 0x9F }; // 20.3mA for all channels as a safe default - TODO: make this configurable or use a safe default? also add standby mode support if off? +static constexpr uint8_t SM16825_SUFFIX[4] = { 0xFF, 0xFF, 0xFF, 0x9F }; // set max current (not really safe but always was the default) +//static constexpr uint8_t SM16825_SUFFIX[4] = { 0x08, 0x42, 0x10, 0x9F }; // 20.3mA for all channels as a safe default - TODO: make this configurable or use a safe default? also add standby mode support if off? // mapBrightnessToCurrentStep() is used by BusDigital for current-based dimming of chips // with discrete current levels (e.g. TM1814/TM1815). From 0bcb677b169b489fc6e5983ffbc07bf977f6b99e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jul 2026 03:19:04 +0200 Subject: [PATCH 170/173] undo fix, made it worse --- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index 356c14159e..a69cfe8c40 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -22,8 +22,8 @@ namespace WLEDpixelBus { // RMT Bus Implementation //============================================================================== -// Per-channel context table - stored in IRAM_ATTR for ISR access -IRAM_ATTR RmtBus::RmtContext RmtBus::s_contexts[WPB_RMT_CHANNELS] = {}; +// Per-channel context table - stored in DRAM, 4 byte aligned for ISR access +DMA_ATTR RmtBus::RmtContext RmtBus::s_contexts[WPB_RMT_CHANNELS] = {}; // Explicit IRAM tranlator callback wrappers for each channel (ensures the function is placed in IRAM which is dropped when using templates) void IRAM_ATTR RmtBus::translator_ch0(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(0, s, d, ss, w, ts, in); } @@ -38,8 +38,8 @@ void IRAM_ATTR RmtBus::translator_ch5(const void* s, rmt_item32_t* d, size_t ss, void IRAM_ATTR RmtBus::translator_ch6(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(6, s, d, ss, w, ts, in); } void IRAM_ATTR RmtBus::translator_ch7(const void* s, rmt_item32_t* d, size_t ss, size_t w, size_t* ts, size_t* in) { translateInternal(7, s, d, ss, w, ts, in); } #endif -// Jump table stored in DRAM so ISR code can quickly find the correct wrapper -IRAM_ATTR const sample_to_rmt_t RmtBus::s_callbacks[WPB_RMT_CHANNELS] = { +// Jump table stored in DRAM, 4 byte aligned so ISR code can quickly find the correct wrapper +DMA_ATTR const sample_to_rmt_t RmtBus::s_callbacks[WPB_RMT_CHANNELS] = { RmtBus::translator_ch0, RmtBus::translator_ch1 #if SOC_RMT_TX_CANDIDATES_PER_GROUP > 2 ,RmtBus::translator_ch2, RmtBus::translator_ch3 From b34425f799c8eab2daee7d1d819d4ab694c8b1b5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jul 2026 09:23:24 +0200 Subject: [PATCH 171/173] bugfix in config page - custom bus is now 39 --- wled00/data/settings_leds.htm | 16 ++++++++-------- wled00/src/WLEDpixelBus/WLEDpixelBus.cpp | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 5b9cee8d83..c102e6d7cc 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -24,6 +24,7 @@ function hasCCT(t) { return !!(gT(t).c & 0x04); } // is white CCT enabled function is16b(t) { return !!(gT(t).c & 0x10); } // is digital 16 bit type function mustR(t) { return !!(gT(t).c & 0x20); } // Off refresh is mandatory + function isCustom(b) { return b === 39; } // check for custom bus type TYPE_CUSTOM_BUS, see const.h function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins function chrID(x) { return String.fromCharCode((x<10?48:55)+x); } function toNum(c) { let n=c.charCodeAt(0); return (n>=48 && n<=57)?n-48:(n>=65 && n<=90)?n-55:0; } // convert char (0-9A-Z) to number (0-35) @@ -398,14 +399,13 @@ } gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory gRGBW |= hasW(t); // RGBW checkbox - gId("co"+n).style.display = (isVir(t) || isAna(t) || isHub75(t) || t==36) ? "none":"inline"; // hide color order for PWM / custom bus - gId("dig"+n+"w").style.display = (isDig(t) && hasW(t) && t!=36) ? "inline":"none"; // show swap channels dropdown (not for custom bus) + gId("co"+n).style.display = (isVir(t) || isAna(t) || isHub75(t) || isCustom(t)) ? "none":"inline"; // hide color order for PWM / custom bus + gId("dig"+n+"w").style.display = (isDig(t) && hasW(t) && !isCustom(t)) ? "inline":"none"; // show swap channels dropdown (not for custom bus) gId("dig"+n+"w").querySelector("[data-opt=CCT]").disabled = !hasCCT(t); // disable WW/CW swapping if (!(isDig(t) && hasW(t))) d.Sf["WO"+n].value = 0; // reset swapping // show/hide custom bus panel and update visible channel rows - const isCustBus = (t === 36); - gId("custbus"+n).style.display = isCustBus ? "inline":"none"; - if (isCustBus) { + gId("custbus"+n).style.display = isCustom(t) ? "inline":"none"; + if (isCustom(t)) { const nch = parseInt(d.Sf["CBch"+n]?.value)||3; for (let ci=0; ci<6; ci++) { let row = gId("CBrow"+n+"_"+ci); @@ -1147,7 +1147,7 @@ // (I2S/LCD/SPI or BitBang) is editable; all others mirror its value and are disabled. // This reflects the hardware constraint: I2S, LCD, SPI, and BitBang all use the same // shared timing and cannot have independent speed factors. - // For custom bus (type 36) on a parallel driver, timing inputs (CBt0h/CBt0l/CBt1h/CBt1l/CBrst) + // For custom bus on a parallel driver, timing inputs (CBt0h/CBt0l/CBt1h/CBt1l/CBrst) // are also shared: only the first custom parallel bus can edit them; others are disabled and mirrored. function syncParallelBusSpeedSelectors() { try { @@ -1160,14 +1160,14 @@ }); let parIndices = []; // all parallel bus IDs (I2S or BitBang) - let custParIndices = []; // subset: custom bus (type 36) on a parallel driver + let custParIndices = []; // subset: custom bus on a parallel driver d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach(sel => { let n = sel.name.substring(2,3); let t = parseInt(sel.value); let drv = d.Sf["LD"+n]?.value | 0; if (isDig(t) && !isD2P(t) && (drv === 1 || drv === 2 || is8266BB(n))) { parIndices.push(n); - if (t === 36) custParIndices.push(n); + if (isCustom(t)) custParIndices.push(n); } }); diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp index 204c6c43bf..51e9b9e1e0 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus.cpp @@ -209,7 +209,8 @@ uint32_t ColorEncoder::decodeGeneric(const uint8_t* in) const { PixelBus* createBus(BusDriver driver, int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType, size_t numPixels) { PixelBus* bus = nullptr; - + // TODO: need to re-order the colorOrder here to match how NPB is mapping the colors or user configs will be wrong. NPB uses IC datasheet orders (to be confirmed) + // for example, SM16825 is RGBWY order on the chip. switch (driver) { #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) case BusDriver::RMT: From 9e2b06c08ccb85b60f10ee106289fe1eb1ef46b7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Jul 2026 12:20:27 +0200 Subject: [PATCH 172/173] cleanup parallel SPI, still rock solid --- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp | 314 +++++++++++------- .../WLEDpixelBus/WLEDpixelBus_ParallelSpi.h | 42 ++- wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp | 2 +- 3 files changed, 219 insertions(+), 139 deletions(-) diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp index 7df8274e69..e4b8dfd017 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.cpp @@ -31,14 +31,12 @@ namespace WLEDpixelBus { //============================================ // low level functions available in IDF V5 (not available in IDF V4, this is for future proofint) -static inline void spi_ll_apply_config(spi_dev_t *hw) -{ +static inline void spi_ll_apply_config(spi_dev_t *hw) { hw->cmd.update = 1; while (hw->cmd.update); //waiting config applied } -static inline void spi_ll_user_start(spi_dev_t *hw) -{ +static inline void spi_ll_user_start(spi_dev_t *hw) { hw->cmd.usr = 1; } @@ -52,6 +50,13 @@ static const int SPI_SIGNAL_INDICES[] = { FSPID_OUT_IDX, FSPIQ_OUT_IDX, FSPIWP_O static constexpr uint16_t SPI_ZERO_BIT = 0x0001; // output: [1,0,0,0] = 25% high (0000 0000 0000 0001 in binary, output LSB first) static constexpr uint16_t SPI_ONE_BIT = 0x0111; // output: [1,1,1,0] = 75% high (0000 0001 0001 0001 in binary, output LSB first) +// Reset pulse: ~300us at ~2.6MHz (4-step cadence). +// 300us * 2.6MHz = 780 bits. We use 1024 bits (~400us) to be safe for all LED types. TODO: is this really safe for all led types? +static constexpr uint32_t SPI_RESET_BITS = 1024; + +// Maximum bits per SPI user transfer (18-bit length register on C3 SPI_MS_DLEN_REG) +static constexpr uint32_t SPI_MAX_BITS = 262143; // note: 4x parallel, 4 steps -> 16bits per source bit, 2kbyte or 680 RGB LEDs max (tested, confirmed) TODO: restrict in UI to 2048 source bytes + SpiBusContext* SpiBusContext::_instance = nullptr; uint8_t SpiBusContext::_refCount = 0; @@ -73,10 +78,9 @@ void SpiBusContext::release() { } SpiBusContext::SpiBusContext() - : _txdone(true) + : _state(SpiState::Idle) , _initialized(false) - //, _hasStarted(false) - , _currentBuffer(0) + , _activeBuffer(0) , _gdmaIsrHandle(nullptr) , _spiIsrHandle(nullptr) , _hw(&GPSPI2) @@ -87,6 +91,7 @@ SpiBusContext::SpiBusContext() , _stagedMask(0) , _channelMask(0) { + _isrMux = portMUX_INITIALIZER_UNLOCKED; for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { _dmaBuffer[i] = nullptr; } @@ -101,28 +106,41 @@ SpiBusContext::~SpiBusContext() { //TODO: rename to isIdle() bool SpiBusContext::isSpiDone() { - if (!_txdone) { - // safety timeout in case of SPI error + if (_state == SpiState::Idle) return true; + + // If we're in an error state, use a timeout to recover. + // This is a safety net for cases where the hardware got stuck + // (e.g. outfifo_empty_err fired but DMA didn't stop cleanly). + if (_state == SpiState::Error) { if (millis() - _lastTransmitMs > 100) { - _txdone = true; - _hw->cmd.usr = 0; forceIdle(); - uint32_t timeout = 10; - while (_hw->cmd.usr && timeout--) { // wait for SPI to actually stop (should be very fast, just in case) - delay(1); - } return true; } - return false; // Still waiting for the cooldown + return false; } - return true; + + // Normal sending states: poll hardware status + // On C3, SPI_TRANS_DONE_INT is the authoritative "transfer complete" signal. + // We also check if the SPI user command bit is still set. + if (_hw->cmd.usr == 0) { + // SPI has stopped. If we were still in a sending state, something + // went wrong (e.g. DMA underrun that didn't trigger the error ISR). + // Transition to error state for cleanup. + if (_state != SpiState::Idle) { + _state = SpiState::Error; + _lastTransmitMs = millis(); + } + return false; // will timeout above + } + + return false; } -// Force SPI idle is a dirty hack to guard against some SPI error/race condition that is really hard to track down, it happens randomly and only if UI refresh plus RMT is involved +// Recovery path for error conditions, cleanly stops DMA, SPI, and disconnects pins to prevent glitches +// TODO: never seeing this being called anymore. needs more testing but can probably be removed void SpiBusContext::forceIdle() { - _txdone = true; - _stagedMask = 0; - +Serial.println("ERROR: forced idle"); + portENTER_CRITICAL(&_isrMux); // make sure no ISR will disturb the sequence // disconnect pins from SPI and set low for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { if (_channels[i].active && _channels[i].pin >= 0) { @@ -132,39 +150,48 @@ void SpiBusContext::forceIdle() { } } if (_hw) { - _hw->cmd.usr = 0; // stop SPI user transfer (will output a fast clock, so detach pins from spi before this to avoid glitches) - _hw->dma_int_clr.val = 0xFFFFFFFF; // clear all SPI interrupt flags + _hw->cmd.usr = 0; + _hw->dma_int_ena.val = 0; // disable all SPI interrupts + _hw->dma_int_clr.val = 0xFFFFFFFF; } + // Stop DMA + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); + + // Reset FIFOs spi_ll_dma_tx_fifo_reset(_hw); spi_ll_outfifo_empty_clr(_hw); - // Stop the DMA - gdma_dev_t* dma = &GDMA; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; // disable interrupt - gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); // reset DMA channel + _state = SpiState::Idle; + _stagedMask = 0; + portEXIT_CRITICAL(&_isrMux); } - //note: using O2 optimization has little to no effect on FPS void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { uint8_t* dst = _dmaBuffer[bufIdx]; uint32_t* dst32 = reinterpret_cast(dst); for (size_t i = 0; i < (WLEDPB_SPI_DMA_BUFFER_SIZE / 4); i++) { dst32[i] = 0; // clear buffer (set all lanes low), DMA buffer is 4 bytes aligned + //memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); // TODO: memset is not ISR IRAM safe? + } + + if (_state == SpiState::WaitingReset) { + return; // No encoding needed; just return and send reset pulse (all zeros) } - //memset(dst, 0x00, WLEDPB_SPI_DMA_BUFFER_SIZE); // TODO: memset is not ISR IRAM safe? -/* - if (!_sending) { - return; - }*/ size_t maxSrcThisChunk = WLEDPB_SPI_DMA_BUFFER_SIZE / 16; // 16 DMA bytes per source byte size_t srcBytesLeft = (_framePos < _numBytes) ? (_numBytes - _framePos) : 0; size_t srcThisChunk = (srcBytesLeft < maxSrcThisChunk) ? srcBytesLeft : maxSrcThisChunk; if (srcThisChunk == 0) { - //_framePos = 0; + // All pixel data has been encoded. Transition to SendingLast state. + // The next buffer fill will be zeroed (reset pulse). + if (_state == SpiState::Sending) { + _state = SpiState::SendingLast; + } return; } @@ -197,69 +224,83 @@ void IRAM_ATTR SpiBusContext::encodeSpiChunk(uint8_t bufIdx) { _framePos += srcThisChunk; } -// SPI ISR: handles trans_done (restart SPI bit counter) and outfifo_empty_err -// (FIFO underrun recovery). outfifo_empty_err is temporarily disabled after -// handling to prevent infinite ISR loops; trans_done re-enables it. +// SPI ISR: handles trans_done (normal completion) and outfifo_empty_err +// (FIFO underrun recovery). Both paths are synchronized with gdmaISR via _isrMux. void IRAM_ATTR SpiBusContext::spiISR(void* arg) { - + SpiBusContext* ctx = (SpiBusContext*)arg; uint32_t status = ctx->_hw->dma_int_st.val; - + ctx->_hw->dma_int_clr.val = status; // Clear all flags immediately if (status & SPI_TRANS_DONE_INT_ST) { - ctx->_txdone = true; // transfer finished - ctx->_hw->dma_int_clr.val = SPI_TRANS_DONE_INT_ST; // clear flag + ctx->_state = SpiState::Idle; // Normal transfer completion. SPI has finished all bits. } - else { - // Serial.println(status, HEX); // -> prints "2" i.e. SPI_OUTFIFO_EMPTY_ERR_INT_ST - ctx->_hw->dma_int_clr.val = status; // Clear all SPI interrupt flags - // SPI FIFO starved, immediately pull the plug or it will output garbage that causes glithces and white flashes + else if (status & SPI_DMA_OUTFIFO_EMPTY_ERR_INT_ST) { + // SPI FIFO starved, ISR latency too high + portENTER_CRITICAL_ISR(&ctx->_isrMux); // note: on C3 this is not really needed as GDMA interrupt has the same priority, keep it just in case + ctx->_state = SpiState::Error; // set Error state so isSpiDone() will timeout and forceIdle() (forceIdle may not be ISR safe) + ctx->_lastTransmitMs = millis(); + + // disconnect pins from SPI to prevent garbage output (calling cmd.usr = 0 outputs a fast clock) for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++){ if (ctx->_channels[i].active && ctx->_channels[i].pin >= 0) { GPIO.func_out_sel_cfg[ctx->_channels[i].pin].func_sel = SIG_GPIO_OUT_IDX; // disconnect from SPI using direct register write (ISR safe) GPIO.out_w1tc.out_w1tc = (1 << ctx->_channels[i].pin); // set ouput low (clear) to avoid glitches note: if implementing this for other ESPs: need to also set the high register for pins >31 } } - // now that pins are disconnected, we can stop the user transfer which is causing the fast clock output that leads to glitches - ctx->_hw->cmd.usr = 0; // stop SPI user transfer TODO: will this result in a tx done or some other interrupt? -> txdone fires - // ctx->_sending = false; - ctx->_txdone = true; // set to tx done, need to reset the spi when checking if spi idle + ctx->_hw->cmd.usr = 0; // stop SPI user transfer + portEXIT_CRITICAL_ISR(&ctx->_isrMux); } - -//digitalWrite(0, HIGH); //!!! note: setting pins inside ISRs can cause crashes - } void IRAM_ATTR SpiBusContext::gdmaISR(void* arg) { SpiBusContext* ctx = (SpiBusContext*)arg; gdma_dev_t* dma = &GDMA; + // Check if this is our interrupt + if (!dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { + return; + } + // make sure we are not disturbed filling the buffer to prevent underruns + portENTER_CRITICAL_ISR(&ctx->_isrMux); + // Clear interrupt immediately + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - // Serial.print("*"); - // toggle gpio0 - // digitalWrite(0, HIGH); - if (dma->intr[WLEDPB_SPI_GDMA_CHANNEL].st.out_eof) { - //Clear interrupt immediately - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; - uint8_t completedBuf = ctx->_currentBuffer; - ctx->encodeSpiChunk(completedBuf); + // If we're idle or in error, ignore spurious interrupts + if (ctx->_state == SpiState::Idle || ctx->_state == SpiState::Error) { + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return; + } - // Give ownership of the descriptor back to DMA so it can keep feeding SPI - ctx->_dmaDesc[completedBuf].eof = 1; // TODO: it works perfectly fine without setting these flags again, does this help with stability? - ctx->_dmaDesc[completedBuf].owner = 1; // give ownership back to DMA: it works without this but just make sure its not passed back to CPU + uint8_t completedBuf = ctx->_activeBuffer; + ctx->_activeBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; - ctx->_currentBuffer = (completedBuf + 1) % WLEDPB_SPI_DMA_DESC_COUNT; // TODO: reduce again to 2 buffers once it is clear that two are enough (probably is) + // If we're in WaitingReset, this EOF means the reset pulse buffer + // has been sent. The transfer is complete. + if (ctx->_state == SpiState::WaitingReset) { + // Stop DMA and SPI cleanly + ctx->hwStopTransfer(); + ctx->_state = SpiState::Idle; + portEXIT_CRITICAL_ISR(&ctx->_isrMux); + return; } - // digitalWrite(0, LOW); - //dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.val = dma->intr[WLEDPB_SPI_GDMA_CHANNEL].raw.val; // clear all GDMA interrupt flags for this channel + + // Fill the completed buffer with next chunk of data (or zeroes for reset) + ctx->encodeSpiChunk(completedBuf); + + // If encodeSpiChunk transitioned to SendingLast, the NEXT buffer after this one is the reset pulse + // when tat EOF fires, we'll be in WaitingReset and stop the transfer + // TODO: this yields long reset pulses, which works but is slower than needed, check if this can be done like in I2S or if that breaks things (it did last time I tried) + + // Give ownership of the descriptor back to DMA so it can keep feeding SPI TODO: this may be unnecessary + ctx->_dmaDesc[completedBuf].eof = 1; + ctx->_dmaDesc[completedBuf].owner = 1; + portEXIT_CRITICAL_ISR(&ctx->_isrMux); } bool SpiBusContext::init(const LedTiming& timing) { if (_initialized) return true; - //pinMode(0, OUTPUT); - //digitalWrite(0, LOW); //!!! - // Allocate DMA buffers for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { _dmaBuffer[i] = (uint8_t*)heap_caps_aligned_alloc(4, WLEDPB_SPI_DMA_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); @@ -370,7 +411,8 @@ bool SpiBusContext::init(const LedTiming& timing) { } void SpiBusContext::deinit() { - _txdone = true; + // Ensure we're in a clean state before freeing resources + forceIdle(); // Stop SPI and DMA before freeing resources if (_hw) { @@ -449,39 +491,19 @@ void SpiBusContext::setChannelData(int8_t channelIdx, const uint8_t* data, size_ if (channelIdx < 0 || channelIdx >= WLEDPB_SPI_MAX_CHANNELS) return; _channels[channelIdx].srcData = data; _channels[channelIdx].srcLen = len; - // Mark this channel as staged _stagedMask |= (1 << channelIdx); - - if (len > _numBytes) _numBytes = len; // update longest bus length } bool SpiBusContext::startTransmit() { - if (_channelCount == 0) return false; // TODO: can this ever happen? - //if (!_txdone) return false; // previous transfer still in progress (should not happen but it might) TODO: does it? -> its guarded + if (_state != SpiState::Idle) return false; // must be idle to start a new frame, skip frame + if (_channelCount == 0) return false; - if (_stagedMask != _channelMask) return true; // not all channels ready yet - _stagedMask = 0; - _txdone = false; - - // debug hack: make sure rmt is not running during critical init - //rmt_channel_t rmtChannel = (rmt_channel_t)0; // TODO: need to track this if using RMT for other purposes, or better, use a separate timer-based approach for WS2812 reset pulse timing - //rmt_wait_tx_done(rmtChannel, portMAX_DELAY); - //rmtChannel= (rmt_channel_t)1; - //rmt_wait_tx_done(rmtChannel, portMAX_DELAY); - // CRITICAL SECTION START: Prevent ISRs from interfering with buffer init - // note: DMA SPI init is VERY timing and sequence sensitive, under load, this sequence is not guaranteed and it leads to stalls and the driver not re-starting correctly - portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; - portENTER_CRITICAL(&mux); - // Stop SPI and DMA before reconfiguring for new transfer - //_hw->cmd.usr = 0; // stop SPI user transfer -> this leads to errors, need to let it "cool down by itself" - uint32_t timeout = 100; - while (_hw->cmd.usr && timeout--) delay(1); // wait for SPI to actually stop (this step is crucial for stability) - spi_ll_clear_int_stat(_hw); // the order of these three commands is important - spi_ll_dma_tx_fifo_reset(_hw); - spi_ll_outfifo_empty_clr(_hw); + // Only start transmission if ALL active channels have populated data + if (_stagedMask != _channelMask) return false; // not all channels staged, something went wrong, skip frame + _stagedMask = 0; // Reset for next frame - // TODO: there is no need to calculate this every frame, could store the value at init + // Calculate actual data length from staged channels size_t newBytes = 0; for (int ch = 0; ch < WLEDPB_SPI_MAX_CHANNELS; ch++) { if (_channels[ch].active && _channels[ch].srcLen > newBytes) { @@ -490,59 +512,97 @@ bool SpiBusContext::startTransmit() { } _numBytes = newBytes; - // Total bits: 16 DMA bytes per source byte × 8 bits/byte = 128 bits per source byte + // Total bits: 16 DMA bytes per source byte * 8 bits/byte = 128 bits per source byte // Plus reset: extra zero bits at the end - uint32_t resetBits = 4096; // TODO: calculate from actual reset pulse, make sure its 4 byte aligned (just in case) // ~300us reset at ~2.6MHz - uint32_t totalBits = (_numBytes * 16 * 8) + resetBits; - if (totalBits > 262143) totalBits = 262143; // SPI: max 18 bits for length register (uninterrupted linked-list DMA transfer) TODO: need to handle this in the UI to not allow more LEDs (restarting a transfer may cause some LED strips to latch, it takes up to 60us) + uint32_t dataBits = _numBytes * 16 * 8; + uint32_t totalBits; - spi_ll_set_mosi_bitlen(_hw, totalBits); + if (dataBits + SPI_RESET_BITS > SPI_MAX_BITS) { + totalBits = SPI_MAX_BITS; // frame does not fit, truncate (cant send more than SPI_MAX_BITS, hardware limitation) + } else { + totalBits = dataBits + SPI_RESET_BITS; // frame fits into max transfer size + } - // re-initialize DMA & buffers + // Wait for SPI to be idle + uint32_t timeout = 100; + while (_hw->cmd.usr && timeout--) { + delay(1); + } + if (_hw->cmd.usr) { + forceIdle(); // SPI is still busy after timeout. Force it idle. + } + + // init hardware, must not be interrupted, otherwise it breaks for some reason + portENTER_CRITICAL(&_isrMux); + _hw->cmd.usr = 0; gdma_dev_t* dma = &GDMA; dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); - gdma_ll_tx_connect_to_periph(dma, WLEDPB_SPI_GDMA_CHANNEL, GDMA_TRIG_PERIPH_SPI, SOC_GDMA_TRIG_PERIPH_SPI2); - gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); + spi_ll_clear_int_stat(_hw); + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + + spi_ll_set_mosi_bitlen(_hw, totalBits); + + // Re-initialize DMA descriptors and encode initial buffers _framePos = 0; - _currentBuffer = 0; + _activeBuffer = 0; + _state = SpiState::Sending; - // fill all descriptors for (int i = 0; i < WLEDPB_SPI_DMA_DESC_COUNT; i++) { + // Restore circular linked list + _dmaDesc[i].qe.stqe_next = &_dmaDesc[(i + 1) % WLEDPB_SPI_DMA_DESC_COUNT]; + _dmaDesc[i].size = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].length = WLEDPB_SPI_DMA_BUFFER_SIZE; + _dmaDesc[i].owner = 1; + _dmaDesc[i].eof = 1; encodeSpiChunk(i); - _dmaDesc[i].owner = 1; // make sure DMA owns the descriptor - _dmaDesc[i].eof = 1; // enable eof flag } + // Phase 4: Brief critical section to start DMA and SPI atomically. + gdma_ll_tx_set_desc_addr(dma, WLEDPB_SPI_GDMA_CHANNEL, (uint32_t)&_dmaDesc[0]); gdma_ll_tx_start(dma, WLEDPB_SPI_GDMA_CHANNEL); - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; // clear again, just in case - dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; // re enable interrupt + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].clr.out_eof = 1; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 1; - // re-attach pins to SPI signals + // Re-attach pins to SPI signals for (int i = 0; i < WLEDPB_SPI_MAX_CHANNELS; i++) { if (_channels[i].active && _channels[i].pin >= 0) { - pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], _channels[i].inverted, false); // TODO: should this "trick" also be used to force outputs low during reset pulse? + pinMatrixOutAttach(_channels[i].pin, SPI_SIGNAL_INDICES[i], _channels[i].inverted, false); } } - spi_ll_dma_tx_enable(_hw, true); spi_ll_apply_config(_hw); // apply SPI config AFTER starting DMA to make sure they are in sync // Short hardware handshake sync: adding a few nops is enough to ensure there is DMA data in the SPI buffer (without this, SPI can immediately quit its duty due to FIFO unterrun) //asm volatile("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"); // might not be needed but just in case - spi_ll_user_start(_hw); // start SPI user transfer - _lastTransmitMs = millis(); - portEXIT_CRITICAL(&mux); -//Serial.print("-"); + portEXIT_CRITICAL(&_isrMux); return true; } -// SpiBus implementation +void SpiBusContext::hwStopTransfer() { + if (_hw) { + _hw->cmd.usr = 0; + _hw->dma_int_ena.val = 0; // Disable all SPI interrupts + } + gdma_dev_t* dma = &GDMA; + dma->intr[WLEDPB_SPI_GDMA_CHANNEL].ena.out_eof = 0; + gdma_ll_tx_reset_channel(dma, WLEDPB_SPI_GDMA_CHANNEL); +} + +void SpiBusContext::hwResetFifo() { + if (_hw) { + spi_ll_dma_tx_fifo_reset(_hw); + spi_ll_outfifo_empty_clr(_hw); + } +} +/////////////////////////////////// +// ParallelSpiBus implementation // +/////////////////////////////////// ParallelSpiBus::ParallelSpiBus(int8_t pin, const LedTiming& timing, uint8_t colorOrder, uint8_t numChannels, uint8_t ledType) : _pin(pin) , _timing(timing) @@ -594,8 +654,8 @@ void ParallelSpiBus::end() { if (_ctx) { uint32_t startWait = millis(); while (!_ctx->isIdle()) { - if (millis() - startWait > 100) { - break; + if (millis() - startWait > 200) { + break; // Timeout: proceed with cleanup anyway } vTaskDelay(1); } @@ -632,7 +692,14 @@ bool ParallelSpiBus::allocateEncodeBuffer(uint16_t numPixels, uint8_t numChannel bool ParallelSpiBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, const CctPixel* /*cct*/) { if (!_initialized || !_ctx || !_encodeBuffer) return false; - while (!_ctx->isSpiDone()) taskYIELD(); + // Wait for previous transmission to complete with timeout (should not happen, BusManager already waits for canShow()) + uint32_t waitStart = millis(); + while (!_ctx->isIdle()) { + if (millis() - waitStart > 200) { + return false; // Timeout: don't start a new frame on a stuck driver + } + vTaskDelay(1); + } _ctx->setChannelData(_channelIdx, _encodeBuffer, _encodeBufferSize); return _ctx->startTransmit(); @@ -640,13 +707,12 @@ bool ParallelSpiBus::show(const uint32_t* /*pixels*/, uint16_t /*numPixels*/, co bool ParallelSpiBus::canShow() const { if (!_ctx) return true; - return _ctx->isSpiDone(); + return _ctx->isIdle(); } void ParallelSpiBus::setColorOrder(uint8_t co) { _encoder = ColorEncoder(co, _encoder.getColorChannels(), _ledType); } - } // namespace WLEDpixelBus #endif // WLEDPB_PARALLEL_SPI_SUPPORT \ No newline at end of file diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h index e4babdefe4..ea2014b6a4 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_ParallelSpi.h @@ -32,9 +32,26 @@ namespace WLEDpixelBus { class ParallelSpiBus; +/** + * SPI driver state machine states. + * Error state is used for recovery from FIFO underrun or other hardware errors. + */ +enum class SpiState : uint8_t { + Idle = 0, // Ready for new frame + Sending = 1, // Data phase active, DMA running + SendingLast = 2, // Last data buffer was filled; waiting for current buffer to finish + WaitingReset = 3, // Reset pulse being sent (zero-filled buffers) + Error = 4 // Error recovery needed (FIFO underrun, timeout, etc.) +}; + /** * SPI bus context - manages SPI2 quad mode for parallel LED output on C3 * Uses GDMA with circular linked-list and ISR-driven buffer refill + + * Error handling: + * If outfifo_empty_err fires, transition to Error state. + * Pins are disconnected and driven low to prevent glitches. + * A 100ms timeout in isSpiDone() will eventually clear Error state. */ class SpiBusContext { public: @@ -50,34 +67,31 @@ class SpiBusContext { bool startTransmit(); bool isSpiDone(); - bool isIdle() { return isSpiDone(); } - void forceIdle(); + bool isIdle() const { return _state == SpiState::Idle; } + void forceIdle(); // emergency stop, disconnects pins, resets hardware void setChannelData(int8_t channelIdx, const uint8_t* data, size_t len); private: SpiBusContext(); ~SpiBusContext(); - void IRAM_ATTR encodeSpiChunk(uint8_t bufIdx); - static void IRAM_ATTR gdmaISR(void* arg); static void IRAM_ATTR spiISR(void* arg); - - volatile bool _txdone; + // Hardware control + void hwStopTransfer(); + void hwResetFifo(); + // State machine + volatile SpiState _state; bool _initialized; - //bool _hasStarted; - volatile uint8_t _currentBuffer; - + volatile uint8_t _activeBuffer; // buffer currently being sent by DMA (like I2S _activeBuffer) // DMA uint8_t* _dmaBuffer[WLEDPB_SPI_DMA_DESC_COUNT]; lldesc_t _dmaDesc[WLEDPB_SPI_DMA_DESC_COUNT]; intr_handle_t _gdmaIsrHandle; intr_handle_t _spiIsrHandle; - - // SPI device - spi_dev_t* _hw; - + portMUX_TYPE _isrMux; + spi_dev_t* _hw; // SPI device // Source data per channel struct ChannelData { ParallelSpiBus* bus; @@ -92,7 +106,7 @@ class SpiBusContext { volatile size_t _framePos; // current source byte position volatile size_t _numBytes; // total source bytes to send mutable uint32_t _lastTransmitMs; - + // Staging: tracks which channels have provided data for the next frame uint8_t _stagedMask; uint8_t _channelMask; diff --git a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp index a69cfe8c40..27aee8bda3 100644 --- a/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp +++ b/wled00/src/WLEDpixelBus/WLEDpixelBus_RMT.cpp @@ -250,7 +250,7 @@ bool RmtBus::show(const uint32_t* pixels, uint16_t numPixels, const CctPixel* cc } else { err = rmt_wait_tx_done(_rmtChannel, 1000 / portTICK_PERIOD_MS); // wait 1s max if (err == ESP_OK) - err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); + err = rmt_write_sample(_rmtChannel, _encodeBuffer, dataLen, false); // CRITICAL BUG: crashes on C3 under heavy UI refresh, this line is reached, then it stalls for some unknown reason } return err; } From 2afc5ebd00fa5a415aeab0b8566095a6bed46531 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 6 Jul 2026 20:36:33 +0200 Subject: [PATCH 173/173] revert unnecessary changes --- build.log | Bin 7190 -> 0 bytes wled00/FXparticleSystem.cpp | 20 +++++++++++--------- wled00/bus_manager.cpp | 3 --- wled00/colors.h | 2 +- wled00/data/favicon.ico | Bin 157 -> 156 bytes 5 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 build.log diff --git a/build.log b/build.log deleted file mode 100644 index fab16bc190f8e2d6f2387e0a07811533edddb60f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7190 zcmd6sYfl?T6o%((EA>CDs49`BzTq^GRIN$?6I37+Nb{jY#MeN;m)ZtGRrRO0eV=nY z?D`UiP%2d`v%9l%&b;S-X88NBz0eQcP!G*8&~+L*;cZCltryB+Nngt0IP8WOp`p=Q zxUlbwp%>0WH4H;b)cw#7Pepa2_o2QF^j6UIR{VwQ9?(t5@4*!~tjVU2wFU6^~}J4w^h zdR5_UY6WCGGa0LTYDgk9)wOvwTbogI04~>D&UBa4cr#pz8eOzQE|kK9@J0AsPyg== zjK2tbU)JkNs-~pFYRHpFwyHFc=o@cMJy-RNbsBoVl(bdd-$=&G@Qt33^-^C`Zf`W& zm9B=G+Z8?QeQj4F{rY;%@LpdBHpe})9j@h%_)e^1P}yXHSGiANm%d0}h2gm6l9^=%Z*N15V0^c`uD3k{u$dy0gw z&NET;V8si4?`STOP_x-cc&d?cy~S0@5AbZT}xzY%F{86%cOcI?r@jRDKPjttR zvXoGz78?pzS96K@y7-WHs zvtyAD_i@hIFIftnAt)n(n!)R{4Z;UWRS|}5X@}g*-tD2G{!BZ5cXL?54J2v`AcSb6v)qy01|GLmU zlT|j2^O~|k-=yo9A1uleZApxksjOAWj#L|}6J*}LvIe$C`stZqajoZMBfP6&HDG+) z^AK6CV>lxPTJCB-v$6b82=G{tywe-j=i5825AB(~we9(IR5{6~O&celF-nCa6VgNF zj9X22Jg#x;c{%BhmFM$xiLS+c*6b)jPLhEB+Bgn{sBNC@J`Q2!-ke}=0)VH%TesOgwAEf)<&v_qgX6y z{*tK36+=;-gl`Smw*0KW+O)Wnv=7rp6tG<*FzyV z7X@`Q?U;zErlG)(bDGhR>}bewB#KP$mgwY(P-Km({i%^u1NhyVX1kZ*J87Q)erD5+ z5p8wdS*4=+vCk-nC&q7_1J{dWFt|OnFm-KmCaeZnA*fxcV z4C0yR4`~7~UKQSJ)AXSv+tUcXgh#kPAwfD96m7F69nPleNM%G(nn|&#`7N_57Ku7# z1?Ka^TcY>ZBx_mRTNV}BmEF&d#i6YXl45i&DThajfQD?cBu{H7nzB49;*4#!70c@x z9$qc7Gq;qt@fqK>H7)k{#BFKR8OQj=&+9;h&wy5>?uXMXT}?1reu#4spu=o!$(>(?tzZPBc3D7 zI)ORT*S47DLc~VxIK1s$JX8ry_BrJ z%Sv~8%*W#K{3$(sT2P#i@7UPoCEAzJZJ;l9Lw*99T4hVC6MJr@g^^v@wa^Q3{Tja4 z<{jqJP8mVtyQF2+(-l>!>>TgB(_IoM z904fe1rA5BY(4MeDFh|eRQ6$fXMZ>wMM-@E-&cfW9INA)&U*LdbM%dV3h22n_UZ7a zTe}?fU-So_OUWnx^tShHfUYCW20h^ivRgJCekg+q6_yHy}s6h{DJQLJ~>m_T0;7I zbcRhm1Anx@{rW;8+jb>ES{IM?excEx{XH^mlIzLZ%}lL3RY^12hN4!fbJLPDm()2P5Z;!KaeC-j_P^)<#$;V^^v z=-#aFc<{NH8*YEcTMmSMO&9&sf#sA#;pPOIJ-Y8{$qXJZ#0uwJ$sJY$BfkytpxTiM zI~EzM je*8^!=6UP()`&CN8=~)+i^w>hb%;B5II~x~t4;m^!cs3g diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index f9034171c7..d0fbc22c5f 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -606,7 +606,7 @@ void ParticleSystem2D::render() { hsv2rgb_spectrum(baseHSV, baseRGB); // convert back to RGB } } - brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution + if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); } @@ -676,12 +676,12 @@ void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex, // - scale brigthness with gamma correction (done in render()) // - apply inverse gamma correction to brightness values // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total - - for (uint32_t i = 0; i < 4; i++) { - pxlbrightness[i] = gamma8inv(pxlbrightness[i]); // use look-up-table for invers gamma + if (gammaCorrectCol) { + for (uint32_t i = 0; i < 4; i++) { + pxlbrightness[i] = gamma8inv(pxlbrightness[i]); // use look-up-table for invers gamma + } } - // standard rendering (2x2 pixels) // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle if (pixco[0].x < 0) { // left pixels out of frame @@ -1463,7 +1463,7 @@ void ParticleSystem1D::render() { hsv2rgb_spectrum(baseHSV, baseRGB); // convert back to RGB } } - brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution + if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution renderParticle(i, brightness, baseRGB, particlesettings.wrap); } // apply smear-blur to rendered frame @@ -1530,8 +1530,10 @@ void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex, // - scale brigthness with gamma correction (done in render()) // - apply inverse gamma correction to brightness values // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total -> fixes brightness fluctuations - pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma - pxlbrightness[1] = gamma8inv(pxlbrightness[1]); + if (gammaCorrectCol) { + pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma + pxlbrightness[1] = gamma8inv(pxlbrightness[1]); + } // check if any pixels are out of frame if (pixco[0] < 0) { // left pixels out of frame @@ -1940,4 +1942,4 @@ static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const return rb | g; } -#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) \ No newline at end of file diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b14a5df13a..eea5403519 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -533,7 +533,6 @@ void BusPwm::setPixelColor(unsigned pix, uint32_t c) { } uint8_t cctWW, cctCW; if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c, cctWW, cctCW); - if (c > 0) c = gamma32(c); // apply gamma correction (table is unity when realtime disables gamma) uint8_t r = R(c), g = G(c), b = B(c), w = W(c); // note: no color scaling, brightness is applied in show() @@ -776,7 +775,6 @@ void BusNetwork::setPixelColor(unsigned pix, uint32_t c) { uint8_t ww, cw; // dummy, unused if (_hasWhite) c = autoWhiteCalc(c, ww, cw); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT - if (c > 0) c = gamma32(c); // apply gamma correction (table is unity when realtime disables gamma) unsigned offset = pix * _UDPchannels; _data[offset] = R(c); _data[offset+1] = G(c); @@ -1171,7 +1169,6 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc. void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel() // if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - if (c > 0) c = gamma32(c); // apply gamma correction (table is unity when realtime disables gamma) if (_ledBuffer) { CRGB fastled_col = CRGB(c); diff --git a/wled00/colors.h b/wled00/colors.h index 1f4ea2e874..00f1b8edf6 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -40,7 +40,7 @@ class NeoGammaWLEDMethod { 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) 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]; // gammaT[] uses unity mapping if gammaCorrectCol is false + 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); } private: diff --git a/wled00/data/favicon.ico b/wled00/data/favicon.ico index 25b07028c05af9613fcd8fb2234726145a278b71..d253350861e52645861732e152003985c4a13efe 100644 GIT binary patch delta 6 NcmbQsIEQh<8~_LC0%!mL delta 8 PcmbQkIG1t497Y8I4A=sR