Skip to content
38 changes: 36 additions & 2 deletions src/main/common/colorconversion.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

#include "stdint.h"
#include "stdbool.h"

#include "color.h"
#include "colorconversion.h"
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

181 changes: 152 additions & 29 deletions src/main/drivers/light_ws2811strip.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;
Expand Down Expand Up @@ -93,16 +110,41 @@ 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;

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);
Expand All @@ -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);
Expand All @@ -132,65 +176,144 @@ 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)
{
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) {
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
lastLowAtUs = micros();
}
}

#endif
7 changes: 1 addition & 6 deletions src/main/drivers/light_ws2811strip.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,14 @@

#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

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);
Expand Down
10 changes: 10 additions & 0 deletions src/main/drivers/timer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/drivers/timer_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading