Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}

164 changes: 135 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,37 @@ 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;

void ws2811LedStripInit(void)
{
const timerHardware_t * timHw = timerGetByTag(IO_TAG(WS2811_PIN), TIM_USE_ANY);
Expand All @@ -120,6 +158,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 +172,131 @@ 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.
static void ws2811StopTransfer(void)
{
timerPWMStopDMA(ws2811TCH);
*timerCCR(ws2811TCH) = 0;
lineIdleLow = true;
lastLowAtUs = micros();
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
}

// 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;
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