Skip to content

Instantly share code, notes, and snippets.

@ChristofKaufmann
Created May 26, 2021 11:17
Show Gist options
  • Save ChristofKaufmann/71a6259a1483df180cde76cb032debe8 to your computer and use it in GitHub Desktop.
Save ChristofKaufmann/71a6259a1483df180cde76cb032debe8 to your computer and use it in GitHub Desktop.
This is a workaround for FastLED library to make SK6812 RGBW led strips work.
/* FastLED_RGBW
*
* Hack to enable SK6812 RGBW strips to work with FastLED.
*
* Original code by Jim Bumgardner (http://krazydad.com).
* Modified by David Madison (http://partsnotincluded.com).
* Modified by Christof Kaufmann.
*
*/
// See: https://www.partsnotincluded.com/fastled-rgbw-neopixels-sk6812/
// See: https://www.dmurph.com/posts/2021/1/cabinet-light-3.html
#pragma once
#include <FastLED.h>
#include <memory> // for std::array in CRGBWArray
/**
* @brief Extends CRGB by white
*
* This contains RGB values and additionally a value for white. This allows to
* use RGBW LEDs with FastLED, but not perfectly. Things like color or
* temperature correction won't work, since it is applied right before sending.
* But this workaround does not give FastLED real RGBW capabilities, so to be
* clear: FastLED still thinks that it is sending color data to RGB LEDs:
* @code
* FastLED sees: R G B|R G B|R G B|R G B|R G B|R G B|
* True LEDs are: R G B W|R G B W|R G B W|R G B W|
* @endcode
*/
struct CRGBW : public CRGB {
union {
uint8_t w;
uint8_t white;
};
CRGBW() : CRGB(0, 0, 0), w{0} {}
CRGBW(uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0)
: CRGB(r, g, b), w{w}
{}
inline void operator=(const CRGB c) __attribute__((always_inline)){
this->r = c.r;
this->g = c.g;
this->b = c.b;
this->w = 0;
}
/**
* @brief Blend two colors in screen mode
*
* This blends two colors nicely giving a bright color.
* Alpha is not supported here, since CRGBW does not support it.
*
* @see https://en.wikipedia.org/wiki/Blend_modes
*/
CRGBW& screen_blend(CRGB b) { // parameter type must be `CRGB` instead of `CRGB const&` due to a bug in operator-
CRGB complement = -(*this);
*this = -(complement.scale8(-b));
return *this;
}
};
CRGB blend(CRGBW a, CRGB const& b) {
return a.screen_blend(b);
}
using CRGBWSet = CPixelView<CRGBW>;
__attribute__((always_inline))
inline CRGBW* operator+(CRGBWSet const& pixels, int offset) {
return (CRGBW*)pixels + offset;
}
/**
* @brief Number of RGB LEDs required to contain the RGBW LEDs values
*
* For details, see #CRGBWArray::rgb_size()
*/
constexpr int rgb_size_from_rgbw_size(int nleds_rgbw){ // original name: getRGBWsize
return nleds_rgbw * 4 / 3 + (nleds_rgbw % 3 != 0);
}
/**
* @brief Calibration factors of the white LED
*
* The values here mean if white is set to 100 (and red, green and blue are
* off), it has the same color and brightness as if red is set to 255, green to
* 140 and blue to 25 (and white is off).
*
* To find these values, start by trying to match the color of white and then
* brightness. The brightness is hard to estimate, so looking at converted
* colors might help.
*/
constexpr float whitepoint_red_factor = 100. / 255.;
constexpr float whitepoint_green_factor = 100. / 140.; ///< @copydoc whitepoint_red_factor
constexpr float whitepoint_blue_factor = 100. / 25.; ///< @copydoc whitepoint_red_factor
/**
* @brief Convert the RGB values to RGBW values in send order
*
* For details, see #CRGBWArray::convert_to_rgbw()
*/
void rgb2rgbw(CRGBW leds[], size_t n_leds) {
for (uint16_t idx = 0; idx < n_leds; ++idx) {
CRGBW& led = leds[idx];
if (led.w > 0)
continue;
// These values are what the 'white' value would need to be to get the corresponding color value.
float white_val_from_red = led.r * whitepoint_red_factor;
float white_val_from_green = led.g * whitepoint_green_factor;
float white_val_from_blue = led.b * whitepoint_blue_factor;
// Set the white value to the highest it can be for the given color
// (without over saturating any channel - thus the minimum of them).
float min_white_value = std::min(white_val_from_red, std::min(white_val_from_green, white_val_from_blue));
// Don't use transformation for very dark values, since rounding errors are visible there
if (min_white_value > 4) {
// The rest of the channels will just be the original value minus the contribution by the white channel.
led.r = static_cast<uint8_t>(std::round(led.r - led.w / whitepoint_red_factor));
led.g = static_cast<uint8_t>(std::round(led.g - led.w / whitepoint_green_factor));
led.b = static_cast<uint8_t>(std::round(led.b - led.w / whitepoint_blue_factor));
led.w = static_cast<uint8_t>(std::floor(min_white_value));
}
// GRBW send order must be taken into account here and set outside to RGB
std::swap(led.r, led.g);
}
}
/// @copydoc rgb2rgbw
template <class LEDArray>
void rgb2rgbw(LEDArray& leds) {
rgb2rgbw(&leds[0], leds.size());
}
/**
* @brief Convert RGBW values back to RGB
*
* This converts the RGBW values back to RGB in case you want to modify them
* instead of completely overwriting them. It also accounts for the send order
* by swapping back red and green values.
*/
void rgbw2rgb(CRGBW leds[], size_t n_leds) {
for (uint16_t idx = 0; idx < n_leds; ++idx) {
CRGBW& led = leds[idx];
// Reverse send order here
CRGBW temp = led;
std::swap(temp.r, temp.g);
// Reverse transformation
led.r = static_cast<uint8_t>(std::max(255.0f, std::round(temp.r + temp.w / whitepoint_red_factor)));
led.g = static_cast<uint8_t>(std::max(255.0f, std::round(temp.g + temp.w / whitepoint_green_factor)));
led.b = static_cast<uint8_t>(std::max(255.0f, std::round(temp.b + temp.w / whitepoint_blue_factor)));
led.w = 0;
}
}
/// @copydoc rgbw2rgb
template <class LEDArray>
void rgbw2rgb(LEDArray& leds) {
rgbw2rgb(&leds[0], leds.size());
}
/**
* @brief CRGBW Array class
*
* Here is an example of how to use it. First, create the array as global
* variable:
* @code
* constexpr int NUM_LEDS = 87;
* CRGBWArray<NUM_LEDS> leds;
* @endcode
*
* Then, configure FastLED to use it (e. g. in the Arduino `setup()` function):
* @code
* constexpr int LED_IO = 19;
* FastLED.addLeds<SK6812, LED_IO, RGB>(leds, leds.rgb_size());
* @endcode
* Note, that the send order must be RGB, although the real send order is GRBW!
* After drawing your effects into `leds`, you have to convert the colors to
* RGBW, which also handles the send order (e. g. in the Arduino `loop()`
* function):
* @code
* leds.convert_to_rgbw();
* FastLED.delay(100); // send during delay
* @endcode
*/
template<int SIZE>
class CRGBWArray : public CPixelView<CRGBW> {
std::array<CRGBW, SIZE> rawleds;
public:
CRGBWArray() : CPixelView<CRGBW>(rawleds.data(), SIZE) {}
using CPixelView::operator=;
/**
* @brief Number of RGB LEDs required to contain the RGBW LEDs values
*
* This is useful for the `FastLED.addLeds()` call, see the example at
* #CRGBWArray.
*
* Example: For 10 RGBW LEDs you need 40 bytes color data, which fits into
* 14 RGB LEDs (42 bytes).
*/
static constexpr int rgb_size() {
return rgb_size_from_rgbw_size(SIZE);
}
/**
* @brief Convert this Array to a CRGB pointer onto the first element
*
* This is useful for the `FastLED.addLeds()` call.
*/
inline operator CRGB* () {
return reinterpret_cast<CRGB*>(rawleds.data());
}
/**
* @brief Convert the RGB values to RGBW values in send order
*
* The part in the RGB values, that can be represented by the white led is
* removed from the RGB values and set to the white value.
*
* Using the equivalent RGBW values reduces power consumption and enhances
* the light quality (RA value). However, it does not allow to go beyond
* the brightness of full RGB. You could set the white value afterwards if
* you really want.
*
* Besides it swaps the red and green channel in the converted values to
* handle the send order.
*/
inline void convert_to_rgbw() {
rgb2rgbw(*this);
}
/**
* @brief Convert the RGBW values in send order back to RGB values
*
* This swaps the red and green channel to revert the send order fix and
* then adds the white led value split into RGB color components back to
* the remained RGB values. This sets the white value to 0.
*
* This function is useful, if you want to modify the led values directly
* in the CRGBWArray.
*/
inline void revert_to_rgb() {
rgbw2rgb(*this);
}
};
#include <FastLED.h>
#include "FastLED_RGBW.h"
constexpr int LED_IO = 19;
constexpr int NUM_LEDS = 87;
CRGBWArray<NUM_LEDS> leds;
void setup() {
// send order set to RGB here, but is indeed GRBW, which is handled by rgb2rgbw()
FastLED.addLeds<SK6812, LED_IO, RGB>(leds, leds.rgb_size());
}
void loop() {
// fill first half of LEDs with a HSV rainbow
static uint8_t hue = 0;
constexpr int HALF_LEDS = NUM_LEDS/2;
for(int i = 0; i <= HALF_LEDS; ++i) {
// set only RGB values
leds[i] = CHSV(hue+i, 200-i, 255);
}
// alternatively use fill_gradient to get the same result as the for loop:
//fill_gradient(&leds[0], HALF_LEDS + 1, CHSV(hue, 200, 255), CHSV(hue + HALF_LEDS, 200 - HALF_LEDS, 255));
++hue;
// CRGBWArray allows for some convenience functions, like copying some RGB values
constexpr int ODD_FIX = NUM_LEDS % 2 - 1; // For even NUM_LEDs: -1, for odd NUM_LEDs: 0
leds(HALF_LEDS, NUM_LEDS-1) = leds(HALF_LEDS + ODD_FIX, 0);
// add moving bar with screen blending
static uint16_t pos = 0;
constexpr uint16_t WIDTH = 4;
for (uint16_t i = 0; i < WIDTH; ++i) {
uint16_t wrapped_pos = (pos + i) % NUM_LEDS;
leds[wrapped_pos].screen_blend(CRGB::FairyLightNCC);
}
if (++pos >= NUM_LEDS)
pos -= NUM_LEDS;
// prepare to send
leds.convert_to_rgbw();
// send
FastLED.delay(100);
// optionally revert the conversion, if you want to modify the values instead of overwriting
//leds.revert_to_rgb();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment