diff --git a/src/main/common/colorconversion.c b/src/main/common/colorconversion.c index a37bc821ab6..7bc4a9f7d61 100644 --- a/src/main/common/colorconversion.c +++ b/src/main/common/colorconversion.c @@ -16,6 +16,7 @@ */ #include "stdint.h" +#include "stdbool.h" #include "color.h" #include "colorconversion.h" @@ -24,9 +25,35 @@ * Source below found here: http://www.kasperkamperman.com/blog/arduino/arduino-programming-hsb-to-rgb/ */ +// Small direct-mapped cache of recent HSV->RGB conversions. LED strip +// patterns tend to reuse a handful of distinct colors across many LEDs and +// updates, so a few cached entries catch most repeats without the cost of a +// full RGB-native color store. Round-robin replacement is enough here: the +// working set per frame is normally <= 4 distinct colors, so eviction order +// doesn't matter much. Not safe for concurrent callers (e.g. one from an +// ISR, one from task context) — currently fine since the sole caller +// (light_ws2811strip.c) never does that. +static struct { + hsvColor_t in; + rgbColor24bpp_t out; + bool valid; +} hsvToRgbCache[4]; +static uint8_t hsvToRgbCacheNextSlot = 0; + +static bool hsvColorEqual(const hsvColor_t *a, const hsvColor_t *b) +{ + return a->h == b->h && a->s == b->s && a->v == b->v; +} + rgbColor24bpp_t* hsvToRgb24(const hsvColor_t* c) { - static rgbColor24bpp_t r; + for (int i = 0; i < 4; i++) { + if (hsvToRgbCache[i].valid && hsvColorEqual(&hsvToRgbCache[i].in, c)) { + return &hsvToRgbCache[i].out; + } + } + + rgbColor24bpp_t r; uint16_t val = c->v; uint16_t sat = 255 - c->s; @@ -79,6 +106,13 @@ rgbColor24bpp_t* hsvToRgb24(const hsvColor_t* c) } } - return &r; + + hsvToRgbCache[hsvToRgbCacheNextSlot].in = *c; + hsvToRgbCache[hsvToRgbCacheNextSlot].out = r; + hsvToRgbCache[hsvToRgbCacheNextSlot].valid = true; + rgbColor24bpp_t *cached = &hsvToRgbCache[hsvToRgbCacheNextSlot].out; + hsvToRgbCacheNextSlot = (hsvToRgbCacheNextSlot + 1) % 4; + + return cached; } diff --git a/src/main/drivers/light_ws2811strip.c b/src/main/drivers/light_ws2811strip.c index cc052fcd872..1bbaae02991 100644 --- a/src/main/drivers/light_ws2811strip.c +++ b/src/main/drivers/light_ws2811strip.c @@ -37,10 +37,14 @@ #include "common/color.h" #include "common/colorconversion.h" +#include "common/maths.h" +#include "common/time.h" #include "drivers/dma.h" #include "drivers/io.h" +#include "drivers/time.h" #include "drivers/timer.h" +#include "drivers/timer_impl.h" #include "drivers/light_ws2811strip.h" #include "fc/runtime_config.h" @@ -49,7 +53,20 @@ #define WS2811_BIT_COMPARE_1 ((WS2811_PERIOD * 2) / 3) #define WS2811_BIT_COMPARE_0 (WS2811_PERIOD / 3) -static DMA_RAM timerDMASafeType_t ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE]; +// Circular DMA buffer: 2 halves, 1 LED group each. ws2811DMARefillCallback +// refills whichever half DMA just finished sending, so this only needs to +// hold a couple of LEDs regardless of strip length. +#define WS2811_LEDS_PER_GROUP 4 +#define WS2811_GROUP_BITS (WS2811_LEDS_PER_GROUP * WS2811_BITS_PER_LED) +#define WS2811_CHUNK_BUFFER_SIZE (2 * WS2811_GROUP_BITS) + +// WS2812 reset/latch is a *minimum* low duration with no upper bound, so +// this is a threshold to check against, not a preamble to budget for. +#define WS2811_RESET_US 60 + +// CCR is a 16-bit register on TIM3/TIM4; DMA must write it at that width or +// the high byte is left stale. +static DMA_RAM uint16_t ledStripDMABuffer[WS2811_CHUNK_BUFFER_SIZE]; static IO_t ws2811IO = IO_NONE; static TCH_t * ws2811TCH = NULL; @@ -93,6 +110,8 @@ void setStripColors(const hsvColor_t *colors) } } +static void ws2811DMARefillCallback(TCH_t * tch, bool transferComplete); + bool ledConfigureDMA(void) { /* Compute the prescaler value */ uint8_t period = WS2811_TIMER_HZ / WS2811_CARRIER_HZ; @@ -100,9 +119,32 @@ bool ledConfigureDMA(void) { timerConfigBase(ws2811TCH, period, WS2811_TIMER_HZ); timerPWMConfigChannel(ws2811TCH, 0); - return timerPWMConfigChannelDMA(ws2811TCH, ledStripDMABuffer, sizeof(ledStripDMABuffer[0]), WS2811_DMA_BUFFER_SIZE); + return timerPWMConfigChannelDMA(ws2811TCH, ledStripDMABuffer, sizeof(ledStripDMABuffer[0]), WS2811_CHUNK_BUFFER_SIZE); } +// Written from both task context and the DMA refill ISR (never truly +// concurrently — the ISR only runs while a transfer is active, and task +// context only touches these once it isn't — but volatile documents that +// and guards against the compiler assuming otherwise). + +// Number of LEDs in the most recent transfer; bounds the DMA transfer (and +// ws2811SetIdleHigh's target) to what's actually configured. +static volatile uint16_t activeLedCount = WS2811_LED_STRIP_LENGTH; + +// groupInHalf[i]: group index currently in half i, so the refill callback +// can tell when the group it just finished sending was the last one. +static volatile uint16_t totalGroups; +static volatile uint16_t nextGroupToAssign; +static volatile uint16_t groupInHalf[2]; + +// Shared between normal transfer completion and ws2811SetIdleHigh (PINIO). +static volatile bool lineIdleLow = true; +static volatile timeUs_t lastLowAtUs = 0; + +// Idle level PINIO last asked for; persists across transfers so +// ws2811StopTransfer() knows what to restore the line to. +static volatile bool idleHighRequested = false; + void ws2811LedStripInit(void) { const timerHardware_t * timHw = timerGetByTag(IO_TAG(WS2811_PIN), TIM_USE_ANY); @@ -120,6 +162,8 @@ void ws2811LedStripInit(void) return; } + impl_timerPWMSetDMARefillCallback(ws2811TCH, ws2811DMARefillCallback); + ws2811IO = IOGetByTag(timHw->tag); //IOGetByTag(IO_TAG(WS2811_PIN)); IOInit(ws2811IO, OWNER_LED_STRIP, RESOURCE_OUTPUT, 0); IOConfigGPIOAF(ws2811IO, IOCFG_AF_PP_FAST, timHw->alternateFunction); @@ -132,9 +176,11 @@ void ws2811LedStripInit(void) // Zero out DMA buffer — LED pin idles LOW between WS2812 bursts memset(&ledStripDMABuffer, 0, sizeof(ledStripDMABuffer)); + lineIdleLow = true; + lastLowAtUs = micros(); ws2811Initialised = true; - ws2811UpdateStrip(); + ws2811UpdateStrip(WS2811_LED_STRIP_LENGTH); } bool isWS2811LedStripReady(void) @@ -142,55 +188,132 @@ bool isWS2811LedStripReady(void) return !timerPWMDMAInProgress(ws2811TCH); } -STATIC_UNIT_TESTED uint16_t dmaBufferOffset; -static int16_t ledIndex; - -STATIC_UNIT_TESTED void fastUpdateLEDDMABuffer(rgbColor24bpp_t *color) +static void writeLedBits(uint16_t *dest, const rgbColor24bpp_t *color) { uint32_t grb = (color->rgb.g << 16) | (color->rgb.r << 8) | (color->rgb.b); for (int8_t index = 23; index >= 0; index--) { - ledStripDMABuffer[WS2811_DELAY_BUFFER_LENGTH + dmaBufferOffset++] = (grb & (1 << index)) ? WS2811_BIT_COMPARE_1 : WS2811_BIT_COMPARE_0; + *dest++ = (grb & (1 << index)) ? WS2811_BIT_COMPARE_1 : WS2811_BIT_COMPARE_0; } } -/* - * This method is non-blocking unless an existing LED update is in progress. - * it does not wait until all the LEDs have been updated, that happens in the background. - */ -void ws2811UpdateStrip(void) +// Slots at or beyond activeLedCount get a direct "off" write instead of a +// color lookup, so a group is self-contained regardless of where the +// configured strip actually ends. +static void ws2811FillGroup(uint8_t halfIndex, uint16_t groupIndex) { - static rgbColor24bpp_t *rgb24; + uint16_t *half = &ledStripDMABuffer[halfIndex * WS2811_GROUP_BITS]; + uint16_t baseLed = groupIndex * WS2811_LEDS_PER_GROUP; + + for (uint8_t slot = 0; slot < WS2811_LEDS_PER_GROUP; slot++) { + uint16_t *dest = &half[slot * WS2811_BITS_PER_LED]; + uint16_t ledIdx = baseLed + slot; + + if (ledIdx < activeLedCount) { + writeLedBits(dest, hsvToRgb24(&ledColorBuffer[ledIdx])); + } else { + for (uint8_t bit = 0; bit < WS2811_BITS_PER_LED; bit++) { + dest[bit] = WS2811_BIT_COMPARE_0; + } + } + } +} - // don't wait - risk of infinite block, just get an update next time round - if (timerPWMDMAInProgress(ws2811TCH)) { +static void ws2811RefillHalf(uint8_t halfIndex) +{ + ws2811FillGroup(halfIndex, nextGroupToAssign); + groupInHalf[halfIndex] = nextGroupToAssign; + nextGroupToAssign++; +} + +// CCR is preload/shadow-buffered, so the direct write below takes effect +// cleanly at the next period boundary without needing further DMA. Restores +// whatever idle level PINIO last asked for, rather than always going low — +// a transfer finishing shouldn't silently override that. +static void ws2811StopTransfer(void) +{ + timerPWMStopDMA(ws2811TCH); + if (idleHighRequested) { + *timerCCR(ws2811TCH) = 255; + lineIdleLow = false; + } else { + *timerCCR(ws2811TCH) = 0; + lineIdleLow = true; + lastLowAtUs = micros(); + } +} + +// transferComplete: true = half 1 just finished (DMA wrapped to half 0), +// false = half 0 just finished (DMA moved on to half 1). +static void ws2811DMARefillCallback(TCH_t * tch, bool transferComplete) +{ + (void)tch; + + uint8_t finishedHalf = transferComplete ? 1 : 0; + + if (groupInHalf[finishedHalf] == totalGroups - 1) { + ws2811StopTransfer(); return; } - dmaBufferOffset = 0; // reset buffer memory index - ledIndex = 0; // reset led index + ws2811RefillHalf(finishedHalf); +} - // fill transmit buffer with correct compare values to achieve - // correct pulse widths according to color values - while (ledIndex < WS2811_LED_STRIP_LENGTH) - { - rgb24 = hsvToRgb24(&ledColorBuffer[ledIndex]); - fastUpdateLEDDMABuffer(rgb24); - ledIndex++; +static void ws2811EnsureResetGap(void) +{ + // A gap before real data is always safe regardless of length — only a + // mid-frame gap (prevented by true circular DMA) risks looking like a + // premature reset — so usually this is just a compare, not a wait. + if (!lineIdleLow || cmpTimeUs(micros(), lastLowAtUs) < WS2811_RESET_US) { + *timerCCR(ws2811TCH) = 0; + lineIdleLow = true; + delayMicroseconds(WS2811_RESET_US); + lastLowAtUs = micros(); } +} - // Initiate hardware transfer +// Non-blocking except when the line was left idle-high by PINIO or updates +// are requested faster than the reset window allows. LEDs are transmitted +// in the background via the DMA refill callback. +void ws2811UpdateStrip(uint16_t usedLedCount) +{ if (!ws2811Initialised || !ws2811TCH) { return; } - timerPWMPrepareDMA(ws2811TCH, WS2811_DMA_BUFFER_SIZE); - timerPWMStartDMA(ws2811TCH); + // don't wait - risk of infinite block, just get an update next time round + if (timerPWMDMAInProgress(ws2811TCH)) { + return; + } + + activeLedCount = MIN(usedLedCount, (uint16_t)WS2811_LED_STRIP_LENGTH); + if (activeLedCount == 0) { + return; + } + + ws2811EnsureResetGap(); + + totalGroups = (activeLedCount + WS2811_LEDS_PER_GROUP - 1) / WS2811_LEDS_PER_GROUP; + nextGroupToAssign = 0; + ws2811RefillHalf(0); + ws2811RefillHalf(1); + + impl_timerPWMSetDMACircular(ws2811TCH, true, WS2811_CHUNK_BUFFER_SIZE); } void ws2811SetIdleHigh(bool high) { - ledStripDMABuffer[WS2811_DMA_BUFFER_SIZE - 1] = high ? 255 : 0; + idleHighRequested = high; // record even if not initialised yet + + if (!ws2811Initialised || !ws2811TCH) { + return; + } + + lineIdleLow = !high; + *timerCCR(ws2811TCH) = high ? 255 : 0; + if (!high) { + lastLowAtUs = micros(); + } } #endif diff --git a/src/main/drivers/light_ws2811strip.h b/src/main/drivers/light_ws2811strip.h index d0edcc276ea..062aafa5441 100644 --- a/src/main/drivers/light_ws2811strip.h +++ b/src/main/drivers/light_ws2811strip.h @@ -23,11 +23,6 @@ #define WS2811_LED_STRIP_LENGTH 128 #define WS2811_BITS_PER_LED 24 -#define WS2811_DELAY_BUFFER_LENGTH 42 // for 50us delay - -#define WS2811_DATA_BUFFER_SIZE (WS2811_BITS_PER_LED * WS2811_LED_STRIP_LENGTH) - -#define WS2811_DMA_BUFFER_SIZE (WS2811_DELAY_BUFFER_LENGTH + WS2811_DATA_BUFFER_SIZE + 1) // leading bytes (reset low 302us) + data bytes LEDS*3 + 1 byte(keep line high optionally) #define WS2811_TIMER_HZ 2400000 #define WS2811_CARRIER_HZ 800000 @@ -35,7 +30,7 @@ void ws2811LedStripInit(void); void ws2811SetIdleHigh(bool high); -void ws2811UpdateStrip(void); +void ws2811UpdateStrip(uint16_t usedLedCount); void setLedHsv(uint16_t index, const hsvColor_t *color); void getLedHsv(uint16_t index, hsvColor_t *color); diff --git a/src/main/drivers/timer.h b/src/main/drivers/timer.h index 8a81b6d5f27..697bddf00a7 100644 --- a/src/main/drivers/timer.h +++ b/src/main/drivers/timer.h @@ -156,6 +156,15 @@ typedef struct timerCallbacks_s { timerCallbackFn * callbackOvr; } timerCallbacks_t; +// Circular-DMA refill callback: invoked from the DMA IRQ while +// dmaState == TCH_DMA_CIRCULAR, once per half-cycle, so the consumer can +// refill the half that was just transmitted. transferComplete is true for +// the TC (second-half-just-sent) event, false for the HT +// (first-half-just-sent) event. Optional (NULL) for circular DMA consumers +// that don't need refilling (e.g. motor DShot idle-packet repeat during +// EEPROM writes) — those get no HT/TC IRQs at all. +typedef void timerDmaRefillFn(struct TCH_s * tch, bool transferComplete); + // Run-time TCH (Timer CHannel) context typedef struct TCH_s { struct timHardwareContext_s * timCtx; // Run-time initialized to parent timer @@ -164,6 +173,7 @@ typedef struct TCH_s { DMA_t dma; // Timer channel DMA handle volatile tchDmaState_e dmaState; void * dmaBuffer; + timerDmaRefillFn * dmaRefillCallback; // optional, see typedef above } TCH_t; // Run-time timer context (dynamically allocated), includes 4x TCH diff --git a/src/main/drivers/timer_impl.h b/src/main/drivers/timer_impl.h index 6a302f57cb4..081569bb70a 100644 --- a/src/main/drivers/timer_impl.h +++ b/src/main/drivers/timer_impl.h @@ -85,6 +85,7 @@ void impl_timerPWMPrepareDMA(TCH_t * tch, uint32_t dmaBufferElementCount); void impl_timerPWMStartDMA(TCH_t * tch); void impl_timerPWMStopDMA(TCH_t * tch); void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferSize); +void impl_timerPWMSetDMARefillCallback(TCH_t * tch, timerDmaRefillFn * callback); #ifdef USE_DSHOT_DMAR bool impl_timerPWMConfigDMABurst(burstDmaTimer_t *burstDmaTimer, TCH_t * tch, void * dmaBuffer, uint8_t dmaBufferElementSize, uint32_t dmaBufferElementCount); diff --git a/src/main/drivers/timer_impl_hal.c b/src/main/drivers/timer_impl_hal.c index a24875eec08..93283368d04 100644 --- a/src/main/drivers/timer_impl_hal.c +++ b/src/main/drivers/timer_impl_hal.c @@ -316,15 +316,31 @@ static inline void LL_TIM_DisableDMAReq_CCx(TIM_TypeDef * TIMx, uint16_t dmaSour static void impl_timerDMA_IRQHandler(DMA_t descriptor) { - if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { - TCH_t * tch = (TCH_t *)descriptor->userParam; + TCH_t * tch = (TCH_t *)descriptor->userParam; + + if (tch->dmaState == TCH_DMA_CIRCULAR) { + // Let DMA keep running - don't disable the stream. HT/TC are only + // enabled here when a refill callback is registered (see + // impl_timerPWMSetDMACircular); non-refilling circular consumers + // never reach this branch. + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_HTIF)) { + DMA_CLEAR_FLAG(descriptor, DMA_IT_HTIF); + if (tch->dmaRefillCallback) { + tch->dmaRefillCallback(tch, false); + } + } - // In circular mode, let DMA keep running - don't disable the stream - if (tch->dmaState == TCH_DMA_CIRCULAR) { + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF); - return; + if (tch->dmaRefillCallback) { + tch->dmaRefillCallback(tch, true); + } } + return; + } + + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { // If it was ACTIVE - switch to IDLE if (tch->dmaState == TCH_DMA_ACTIVE) { tch->dmaState = TCH_DMA_IDLE; @@ -337,6 +353,11 @@ static void impl_timerDMA_IRQHandler(DMA_t descriptor) } } +void impl_timerPWMSetDMARefillCallback(TCH_t * tch, timerDmaRefillFn * callback) +{ + tch->dmaRefillCallback = callback; +} + bool impl_timerPWMConfigChannelDMA(TCH_t * tch, void * dmaBuffer, uint8_t dmaBufferElementSize, uint32_t dmaBufferElementCount) { tch->dma = dmaGetByTag(tch->timHw->dmaTag); @@ -634,6 +655,13 @@ void impl_timerPWMStopDMA(TCH_t * tch) ATOMIC_BLOCK(NVIC_PRIO_MAX) { LL_TIM_DisableDMAReq_CCx(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex]); LL_DMA_DisableStream(dmaBase, streamLL); + + // STM32H7 RM: poll EN bit until stream is actually disabled + uint32_t timeout = 10000; // ~20us at 480MHz, well above worst-case disable latency + while (LL_DMA_IsEnabledStream(dmaBase, streamLL) && timeout--) { + __NOP(); + } + DMA_CLEAR_FLAG(tch->dma, DMA_IT_TCIF); } tch->dmaState = TCH_DMA_IDLE; @@ -672,12 +700,20 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferS LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_CIRCULAR); // Circular mode requires non-zero NDTR (STM32H7 RM constraint) LL_DMA_SetDataLength(dmaBase, streamLL, dmaBufferSize); - // Disable TC interrupt — in circular mode, TC fires every cycle - // and the IRQ handler would otherwise disable the stream - LL_DMA_DisableIT_TC(dmaBase, streamLL); + if (tch->dmaRefillCallback) { + // Refill consumer needs an IRQ every half-cycle to keep the + // buffer fed + LL_DMA_EnableIT_HT(dmaBase, streamLL); + LL_DMA_EnableIT_TC(dmaBase, streamLL); + } else { + // Disable TC interrupt — in circular mode, TC fires every cycle + // and the IRQ handler would otherwise disable the stream + LL_DMA_DisableIT_TC(dmaBase, streamLL); + } tch->dmaState = TCH_DMA_CIRCULAR; } else { LL_DMA_SetMode(dmaBase, streamLL, LL_DMA_MODE_NORMAL); + LL_DMA_DisableIT_HT(dmaBase, streamLL); LL_DMA_EnableIT_TC(dmaBase, streamLL); tch->dmaState = TCH_DMA_IDLE; } diff --git a/src/main/drivers/timer_impl_stdperiph.c b/src/main/drivers/timer_impl_stdperiph.c index 13b38cdd2b3..81e6c4d9fd1 100644 --- a/src/main/drivers/timer_impl_stdperiph.c +++ b/src/main/drivers/timer_impl_stdperiph.c @@ -268,15 +268,31 @@ void impl_timerChCaptureCompareEnable(TCH_t * tch, bool enable) static void impl_timerDMA_IRQHandler(DMA_t descriptor) { - if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { - TCH_t * tch = (TCH_t *)descriptor->userParam; + TCH_t * tch = (TCH_t *)descriptor->userParam; + + if (tch->dmaState == TCH_DMA_CIRCULAR) { + // Let DMA keep running - don't disable the stream. HT/TC are only + // enabled here when a refill callback is registered (see + // impl_timerPWMSetDMACircular); non-refilling circular consumers + // never reach this branch. + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_HTIF)) { + DMA_CLEAR_FLAG(descriptor, DMA_IT_HTIF); + if (tch->dmaRefillCallback) { + tch->dmaRefillCallback(tch, false); + } + } - // In circular mode, let DMA keep running - don't disable the stream - if (tch->dmaState == TCH_DMA_CIRCULAR) { + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF); - return; + if (tch->dmaRefillCallback) { + tch->dmaRefillCallback(tch, true); + } } + return; + } + + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { tch->dmaState = TCH_DMA_IDLE; TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], DISABLE); @@ -286,6 +302,11 @@ static void impl_timerDMA_IRQHandler(DMA_t descriptor) } } +void impl_timerPWMSetDMARefillCallback(TCH_t * tch, timerDmaRefillFn * callback) +{ + tch->dmaRefillCallback = callback; +} + bool impl_timerPWMConfigChannelDMA(TCH_t * tch, void * dmaBuffer, uint8_t dmaBufferElementSize, uint32_t dmaBufferElementCount) { DMA_InitTypeDef DMA_InitStructure; @@ -563,6 +584,13 @@ void impl_timerPWMStopDMA(TCH_t * tch) { TIM_DMACmd(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], DISABLE); DMA_Cmd(tch->dma->ref, DISABLE); + + // STM32F4/F7 RM: poll EN bit until stream is actually disabled + uint32_t timeout = 10000; // ~60us at 168MHz, well above worst-case disable latency + while ((tch->dma->ref->CR & DMA_SxCR_EN) && timeout--) { + __NOP(); + } + tch->dmaState = TCH_DMA_IDLE; TIM_Cmd(tch->timHw->tim, ENABLE); } @@ -594,12 +622,19 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferS if (circular) { tch->dma->ref->CR |= DMA_SxCR_CIRC; DMA_SetCurrDataCounter(tch->dma->ref, dmaBufferSize); - // Disable TC interrupt — in circular mode, TC fires every cycle - // and the IRQ handler would otherwise disable the stream - DMA_ITConfig(tch->dma->ref, DMA_IT_TC, DISABLE); + if (tch->dmaRefillCallback) { + // Refill consumer needs an IRQ every half-cycle to keep the + // buffer fed + DMA_ITConfig(tch->dma->ref, DMA_IT_HT | DMA_IT_TC, ENABLE); + } else { + // Disable TC interrupt — in circular mode, TC fires every cycle + // and the IRQ handler would otherwise disable the stream + DMA_ITConfig(tch->dma->ref, DMA_IT_TC, DISABLE); + } tch->dmaState = TCH_DMA_CIRCULAR; } else { tch->dma->ref->CR &= ~DMA_SxCR_CIRC; + DMA_ITConfig(tch->dma->ref, DMA_IT_HT, DISABLE); DMA_ITConfig(tch->dma->ref, DMA_IT_TC, ENABLE); tch->dmaState = TCH_DMA_IDLE; } diff --git a/src/main/drivers/timer_impl_stdperiph_at32.c b/src/main/drivers/timer_impl_stdperiph_at32.c index 54c6d257078..0bc26b0d613 100644 --- a/src/main/drivers/timer_impl_stdperiph_at32.c +++ b/src/main/drivers/timer_impl_stdperiph_at32.c @@ -267,15 +267,31 @@ void impl_timerChCaptureCompareEnable(TCH_t * tch, bool enable) // lookupDMASourceTable static void impl_timerDMA_IRQHandler(DMA_t descriptor) { - if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { - TCH_t * tch = (TCH_t *)descriptor->userParam; + TCH_t * tch = (TCH_t *)descriptor->userParam; + + if (tch->dmaState == TCH_DMA_CIRCULAR) { + // Let DMA keep running - don't disable the channel. HT/TC are only + // enabled here when a refill callback is registered (see + // impl_timerPWMSetDMACircular); non-refilling circular consumers + // never reach this branch. + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_HTIF)) { + DMA_CLEAR_FLAG(descriptor, DMA_IT_HTIF); + if (tch->dmaRefillCallback) { + tch->dmaRefillCallback(tch, false); + } + } - // In circular mode, let DMA keep running - don't disable the channel - if (tch->dmaState == TCH_DMA_CIRCULAR) { + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { DMA_CLEAR_FLAG(descriptor, DMA_IT_TCIF); - return; + if (tch->dmaRefillCallback) { + tch->dmaRefillCallback(tch, true); + } } + return; + } + + if (DMA_GET_FLAG_STATUS(descriptor, DMA_IT_TCIF)) { tch->dmaState = TCH_DMA_IDLE; dma_channel_enable(tch->dma->ref,FALSE); tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], FALSE); @@ -283,6 +299,11 @@ static void impl_timerDMA_IRQHandler(DMA_t descriptor) } } +void impl_timerPWMSetDMARefillCallback(TCH_t * tch, timerDmaRefillFn * callback) +{ + tch->dmaRefillCallback = callback; +} + bool impl_timerPWMConfigChannelDMA(TCH_t * tch, void * dmaBuffer, uint8_t dmaBufferElementSize, uint32_t dmaBufferElementCount) { dma_init_type dma_init_struct = {0}; @@ -411,6 +432,13 @@ void impl_timerPWMStopDMA(TCH_t * tch) { tmr_dma_request_enable(tch->timHw->tim, lookupDMASourceTable[tch->timHw->channelIndex], FALSE); dma_channel_enable(tch->dma->ref,FALSE); + + // AT32: poll enable bit until channel is actually disabled + uint32_t timeout = 10000; // ~40us at 288MHz, well above worst-case disable latency + while (tch->dma->ref->ctrl_bit.chen && timeout--) { + __NOP(); + } + tch->dmaState = TCH_DMA_IDLE; tmr_counter_enable(tch->timHw->tim, TRUE); } @@ -442,12 +470,20 @@ void impl_timerPWMSetDMACircular(TCH_t * tch, bool circular, uint32_t dmaBufferS if (circular) { tch->dma->ref->ctrl_bit.lm = TRUE; dma_data_number_set(tch->dma->ref, dmaBufferSize); - // Disable TC interrupt — in circular mode, TC fires every cycle - // and the IRQ handler would otherwise disable the channel - dma_interrupt_enable(tch->dma->ref, DMA_IT_TCIF, FALSE); + if (tch->dmaRefillCallback) { + // Refill consumer needs an IRQ every half-cycle to keep the + // buffer fed + dma_interrupt_enable(tch->dma->ref, DMA_IT_HTIF, TRUE); + dma_interrupt_enable(tch->dma->ref, DMA_IT_TCIF, TRUE); + } else { + // Disable TC interrupt — in circular mode, TC fires every cycle + // and the IRQ handler would otherwise disable the channel + dma_interrupt_enable(tch->dma->ref, DMA_IT_TCIF, FALSE); + } tch->dmaState = TCH_DMA_CIRCULAR; } else { tch->dma->ref->ctrl_bit.lm = FALSE; + dma_interrupt_enable(tch->dma->ref, DMA_IT_HTIF, FALSE); dma_interrupt_enable(tch->dma->ref, DMA_IT_TCIF, TRUE); tch->dmaState = TCH_DMA_IDLE; } diff --git a/src/main/io/ledstrip.c b/src/main/io/ledstrip.c index 8e8e5771450..7169cf6cfb9 100644 --- a/src/main/io/ledstrip.c +++ b/src/main/io/ledstrip.c @@ -989,7 +989,7 @@ void ledStripUpdate(timeUs_t currentTimeUs) bool updateNow = timActive & (1 << timId); (*layerTable[timId])(updateNow, timer); } - ws2811UpdateStrip(); + ws2811UpdateStrip(ledCounts.count); } bool parseColor(int index, const char *colorConfig) @@ -1077,6 +1077,6 @@ static void ledStripDisable(void) { setStripColor(&HSV(BLACK)); - ws2811UpdateStrip(); + ws2811UpdateStrip(ledCounts.count); } #endif