Implements 5bit hd gamma correction using the bit shift method for APA102 and SK9822 chipsets

This commit is contained in:
zackees 2023-09-29 14:10:35 -07:00 committed by zackees
parent 3d2ab780b4
commit a36b331703
6 changed files with 424 additions and 67 deletions

View File

@ -17,7 +17,7 @@
set -eou pipefail
# List of examples that will be compiled by default
EXAMPLES=${EXAMPLES:-"Blink ColorPalette ColorTemperature Cylon DemoReel100
EXAMPLES=${EXAMPLES:-"Apa102HD Blink ColorPalette ColorTemperature Cylon DemoReel100
Fire2012 FirstLight Multiple/MultipleStripsInOneArray
Multiple/ArrayOfLedArrays Noise NoisePlayground NoisePlusPalette Pacifica
Pride2015 RGBCalibrate RGBSetDemo TwinkleFox XYMatrix"}

View File

@ -0,0 +1,47 @@
/// @file Apa102HD.ino
/// @brief Example showing how to use the APA102HD gamma correction.
/// @example Apa102HD.ino
#include <Arduino.h>
#include <FastLED.h>
#define NUM_LEDS 20
static bool gamma_function_hit = false;
CRGB leds_hd[NUM_LEDS] = {0}; // HD mode implies gamma.
CRGB leds[NUM_LEDS] = {0}; // Software gamma mode.
uint8_t gamma8(uint8_t x) {
return uint8_t((uint16_t(x) * x) >> 8);
}
CRGB gammaCorrect(CRGB c) {
c.r = gamma8(c.r);
c.g = gamma8(c.g);
c.b = gamma8(c.b);
return c;
}
void setup() {
delay(500); // power-up safety delay
FastLED.addLeds<APA102HD, 1, 2, RGB>(leds_hd, NUM_LEDS);
FastLED.addLeds<APA102, 3, 4, RGB>(leds, NUM_LEDS);
}
void loop() {
uint32_t now = millis();
uint32_t t = now / 100;
for (int i = 0; i < NUM_LEDS; i++) {
uint8_t brightness = sin8(t + i);
CRGB c(brightness, brightness, brightness);
leds_hd[i] = c;
leds[i] = gammaCorrect(c);
}
FastLED.show();
delay(8);
if (!gamma_function_hit) {
Serial.println("gamma function not hit");
}
}

View File

@ -87,7 +87,9 @@ enum ESPIChipsets {
P9813, ///< P9813 LED chipset
APA102, ///< APA102 LED chipset
SK9822, ///< SK9822 LED chipset
DOTSTAR ///< APA102 LED chipset alias
SK9822HD, ///< SK9822 LED chipset with 5-bit gamma correction
DOTSTAR, ///< APA102 LED chipset alias
APA102HD, ///< APA102 LED chipset with 5-bit gamma correction
};
/// Smart Matrix Library controller type
@ -274,7 +276,9 @@ public:
case P9813: { static P9813Controller<DATA_PIN, CLOCK_PIN, RGB_ORDER, SPI_DATA_RATE> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case DOTSTAR:
case APA102: { static APA102Controller<DATA_PIN, CLOCK_PIN, RGB_ORDER, SPI_DATA_RATE> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case APA102HD: { static APA102ControllerHD<DATA_PIN, CLOCK_PIN, RGB_ORDER, SPI_DATA_RATE> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case SK9822: { static SK9822Controller<DATA_PIN, CLOCK_PIN, RGB_ORDER, SPI_DATA_RATE> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case SK9822HD: { static SK9822ControllerHD<DATA_PIN, CLOCK_PIN, RGB_ORDER, SPI_DATA_RATE> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
}
}
@ -289,7 +293,9 @@ public:
case P9813: { static P9813Controller<DATA_PIN, CLOCK_PIN> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case DOTSTAR:
case APA102: { static APA102Controller<DATA_PIN, CLOCK_PIN> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case APA102HD: { static APA102ControllerHD<DATA_PIN, CLOCK_PIN> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case SK9822: { static SK9822Controller<DATA_PIN, CLOCK_PIN> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case SK9822HD: { static SK9822ControllerHD<DATA_PIN, CLOCK_PIN> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
}
}
@ -304,7 +310,9 @@ public:
case P9813: { static P9813Controller<DATA_PIN, CLOCK_PIN, RGB_ORDER> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case DOTSTAR:
case APA102: { static APA102Controller<DATA_PIN, CLOCK_PIN, RGB_ORDER> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case APA102HD: { static APA102ControllerHD<DATA_PIN, CLOCK_PIN, RGB_ORDER> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case SK9822: { static SK9822Controller<DATA_PIN, CLOCK_PIN, RGB_ORDER> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
case SK9822HD: { static SK9822ControllerHD<DATA_PIN, CLOCK_PIN, RGB_ORDER> c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); }
}
}

View File

@ -3,6 +3,7 @@
#include "FastLED.h"
#include "pixeltypes.h"
#include "five_bit_hd_gamma.h"
/// @file chipsets.h
/// Contains the bulk of the definitions for the various LED chipsets supported.
@ -214,13 +215,35 @@ protected:
/// @tparam CLOCK_PIN the clock pin for these LEDs
/// @tparam RGB_ORDER the RGB ordering for these LEDs
/// @tparam SPI_SPEED the clock divider used for these LEDs. Set using the ::DATA_RATE_MHZ / ::DATA_RATE_KHZ macros. Defaults to ::DATA_RATE_MHZ(12)
template <uint8_t DATA_PIN, uint8_t CLOCK_PIN, EOrder RGB_ORDER = RGB, uint32_t SPI_SPEED = DATA_RATE_MHZ(12)>
template <
uint8_t DATA_PIN, uint8_t CLOCK_PIN,
EOrder RGB_ORDER = RGB,
uint32_t SPI_SPEED = DATA_RATE_MHZ(12),
FiveBitGammaCorrectionMode GAMMA_CORRECTION_MODE = kFiveBitGammaCorrectionMode_Null,
uint32_t START_FRAME = 0x00000000,
uint32_t END_FRAME = 0xFF000000
>
class APA102Controller : public CPixelLEDController<RGB_ORDER> {
typedef SPIOutput<DATA_PIN, CLOCK_PIN, SPI_SPEED> SPI;
SPI mSPI;
void startBoundary() { mSPI.writeWord(0); mSPI.writeWord(0); }
void endBoundary(int nLeds) { int nDWords = (nLeds/32); do { mSPI.writeByte(0xFF); mSPI.writeByte(0x00); mSPI.writeByte(0x00); mSPI.writeByte(0x00); } while(nDWords--); }
void startBoundary() {
mSPI.writeWord(START_FRAME >> 16);
mSPI.writeWord(START_FRAME & 0xFFFF);
}
void endBoundary(int nLeds) {
int nDWords = (nLeds/32);
const uint8_t b0 = uint8_t(END_FRAME >> 24 & 0x000000ff);
const uint8_t b1 = uint8_t(END_FRAME >> 16 & 0x000000ff);
const uint8_t b2 = uint8_t(END_FRAME >> 8 & 0x000000ff);
const uint8_t b3 = uint8_t(END_FRAME >> 0 & 0x000000ff);
do {
mSPI.writeByte(b0);
mSPI.writeByte(b1);
mSPI.writeByte(b2);
mSPI.writeByte(b3);
} while(nDWords--);
}
inline void writeLed(uint8_t brightness, uint8_t b0, uint8_t b1, uint8_t b2) __attribute__((always_inline)) {
#ifdef FASTLED_SPI_BYTE_ONLY
@ -237,6 +260,15 @@ class APA102Controller : public CPixelLEDController<RGB_ORDER> {
#endif
}
inline void write2Bytes(uint8_t b1, uint8_t b2) __attribute__((always_inline)) {
#ifdef FASTLED_SPI_BYTE_ONLY
mSPI.writeByte(b1);
mSPI.writeByte(b2);
#else
mSPI.writeWord(uint16_t(b1) << 8 | b2);
#endif
}
public:
APA102Controller() {}
@ -247,9 +279,26 @@ public:
protected:
/// @copydoc CPixelLEDController::showPixels()
virtual void showPixels(PixelController<RGB_ORDER> & pixels) {
mSPI.select();
switch (GAMMA_CORRECTION_MODE) {
case kFiveBitGammaCorrectionMode_Null: {
showPixelsDefault(pixels);
break;
}
case kFiveBitGammaCorrectionMode_BitShift: {
showPixelsGammaBitShift(pixels);
break;
}
}
}
uint8_t s0 = pixels.getScale0(), s1 = pixels.getScale1(), s2 = pixels.getScale2();
private:
static inline void getGlobalBrightnessAndScalingFactors(
PixelController<RGB_ORDER>& pixels,
uint8_t* out_s0, uint8_t* out_s1, uint8_t* out_s2, uint8_t* out_brightness) {
uint8_t s0 = pixels.getScale0();
uint8_t s1 = pixels.getScale1();
uint8_t s2 = pixels.getScale2();
#if FASTLED_USE_GLOBAL_BRIGHTNESS == 1
const uint16_t maxBrightness = 0x1F;
uint16_t brightness = ((((uint16_t)max(max(s0, s1), s2) + 1) * maxBrightness - 1) >> 8) + 1;
@ -259,10 +308,23 @@ protected:
#else
const uint8_t brightness = 0x1F;
#endif
*out_s0 = s0;
*out_s1 = s1;
*out_s2 = s2;
*out_brightness = static_cast<uint8_t>(brightness);
}
// Legacy showPixels implementation.
inline void showPixelsDefault(PixelController<RGB_ORDER> & pixels) {
mSPI.select();
uint8_t s0, s1, s2, global_brightness;
getGlobalBrightnessAndScalingFactors(pixels, &s0, &s1, &s2, &global_brightness);
startBoundary();
while (pixels.has(1)) {
writeLed(brightness, pixels.loadAndScale0(0, s0), pixels.loadAndScale1(0, s1), pixels.loadAndScale2(0, s2));
uint8_t r = pixels.loadAndScale0(0, s0);
uint8_t g = pixels.loadAndScale1(0, s1);
uint8_t b = pixels.loadAndScale2(0, s2);
writeLed(global_brightness, r, g, b);
pixels.stepDithering();
pixels.advanceData();
}
@ -272,72 +334,96 @@ protected:
mSPI.release();
}
inline void showPixelsGammaBitShift(PixelController<RGB_ORDER> & pixels) {
mSPI.select();
uint8_t s0, s1, s2, global_brightness;
getGlobalBrightnessAndScalingFactors(pixels, &s0, &s1, &s2, &global_brightness);
startBoundary();
while (pixels.has(1)) {
uint8_t r = pixels.loadAndScale0(0, s0);
uint8_t g = pixels.loadAndScale1(0, s1);
uint8_t b = pixels.loadAndScale2(0, s2);
uint8_t brightness = 0;
five_bit_hd_gamma_bitshift(r, g, b, &r, &g, &b, &brightness);
if (global_brightness >= 0x1F) {
// 5-bit mix.
brightness = static_cast<uint8_t>(
(uint16_t(brightness) * global_brightness)
/ 0x1F
);
}
writeLed(brightness, r, g, b);
pixels.stepDithering();
pixels.advanceData();
}
endBoundary(pixels.size());
mSPI.waitFully();
mSPI.release();
}
};
/// SK9822 controller class.
template <
uint8_t DATA_PIN,
uint8_t CLOCK_PIN,
EOrder RGB_ORDER = RGB,
uint32_t SPI_SPEED = DATA_RATE_MHZ(24)
>
class APA102ControllerHD : public APA102Controller<
DATA_PIN,
CLOCK_PIN,
RGB_ORDER,
SPI_SPEED,
kFiveBitGammaCorrectionMode_BitShift,
uint32_t(0x00000000),
uint32_t(0x00000000)> {
public:
APA102ControllerHD() = default;
APA102ControllerHD(const APA102ControllerHD&) = delete;
};
/// SK9822 controller class. It's exactly the same as the APA102Controller protocol but with a different END_FRAME and default SPI_SPEED.
/// @tparam DATA_PIN the data pin for these LEDs
/// @tparam CLOCK_PIN the clock pin for these LEDs
/// @tparam RGB_ORDER the RGB ordering for these LEDs
/// @tparam SPI_SPEED the clock divider used for these LEDs. Set using the ::DATA_RATE_MHZ / ::DATA_RATE_KHZ macros. Defaults to ::DATA_RATE_MHZ(24)
template <uint8_t DATA_PIN, uint8_t CLOCK_PIN, EOrder RGB_ORDER = RGB, uint32_t SPI_SPEED = DATA_RATE_MHZ(24)>
class SK9822Controller : public CPixelLEDController<RGB_ORDER> {
typedef SPIOutput<DATA_PIN, CLOCK_PIN, SPI_SPEED> SPI;
SPI mSPI;
void startBoundary() { mSPI.writeWord(0); mSPI.writeWord(0); }
void endBoundary(int nLeds) { int nLongWords = (nLeds/32); do { mSPI.writeByte(0x00); mSPI.writeByte(0x00); mSPI.writeByte(0x00); mSPI.writeByte(0x00); } while(nLongWords--); }
inline void writeLed(uint8_t brightness, uint8_t b0, uint8_t b1, uint8_t b2) __attribute__((always_inline)) {
#ifdef FASTLED_SPI_BYTE_ONLY
mSPI.writeByte(0xE0 | brightness);
mSPI.writeByte(b0);
mSPI.writeByte(b1);
mSPI.writeByte(b2);
#else
uint16_t b = 0xE000 | (brightness << 8) | (uint16_t)b0;
mSPI.writeWord(b);
uint16_t w = b1 << 8;
w |= b2;
mSPI.writeWord(w);
#endif
}
public:
SK9822Controller() {}
virtual void init() {
mSPI.init();
}
protected:
/// @copydoc CPixelLEDController::showPixels()
virtual void showPixels(PixelController<RGB_ORDER> & pixels) {
mSPI.select();
uint8_t s0 = pixels.getScale0(), s1 = pixels.getScale1(), s2 = pixels.getScale2();
#if FASTLED_USE_GLOBAL_BRIGHTNESS == 1
const uint16_t maxBrightness = 0x1F;
uint16_t brightness = ((((uint16_t)max(max(s0, s1), s2) + 1) * maxBrightness - 1) >> 8) + 1;
s0 = (maxBrightness * s0 + (brightness >> 1)) / brightness;
s1 = (maxBrightness * s1 + (brightness >> 1)) / brightness;
s2 = (maxBrightness * s2 + (brightness >> 1)) / brightness;
#else
const uint8_t brightness = 0x1F;
#endif
startBoundary();
while (pixels.has(1)) {
writeLed(brightness, pixels.loadAndScale0(0, s0), pixels.loadAndScale1(0, s1), pixels.loadAndScale2(0, s2));
pixels.stepDithering();
pixels.advanceData();
}
endBoundary(pixels.size());
mSPI.waitFully();
mSPI.release();
}
template <
uint8_t DATA_PIN,
uint8_t CLOCK_PIN,
EOrder RGB_ORDER = RGB,
uint32_t SPI_SPEED = DATA_RATE_MHZ(24)
>
class SK9822Controller : public APA102Controller<
DATA_PIN,
CLOCK_PIN,
RGB_ORDER,
SPI_SPEED,
kFiveBitGammaCorrectionMode_Null,
0x00000000,
0x00000000
> {
};
/// SK9822 controller class. It's exactly the same as the APA102Controller protocol but with a different END_FRAME and default SPI_SPEED.
/// @tparam DATA_PIN the data pin for these LEDs
/// @tparam CLOCK_PIN the clock pin for these LEDs
/// @tparam RGB_ORDER the RGB ordering for these LEDs
/// @tparam SPI_SPEED the clock divider used for these LEDs. Set using the ::DATA_RATE_MHZ / ::DATA_RATE_KHZ macros. Defaults to ::DATA_RATE_MHZ(24)
template <
uint8_t DATA_PIN,
uint8_t CLOCK_PIN,
EOrder RGB_ORDER = RGB,
uint32_t SPI_SPEED = DATA_RATE_MHZ(24)
>
class SK9822ControllerHD : public APA102Controller<
DATA_PIN,
CLOCK_PIN,
RGB_ORDER,
SPI_SPEED,
kFiveBitGammaCorrectionMode_BitShift,
0x00000000,
0x00000000
> {
};

162
src/five_bit_hd_gamma.cpp Normal file
View File

@ -0,0 +1,162 @@
#include "FastLED.h"
#include "five_bit_hd_gamma.h"
FASTLED_NAMESPACE_BEGIN
__attribute__((weak))
void five_bit_hd_gamma_function(
uint8_t r8, uint8_t g8, uint8_t b8,
uint16_t* r16, uint16_t* g16, uint16_t* b16) {
*r16 = uint16_t(r8) * r8;
*g16 = uint16_t(g8) * g8;
*b16 = uint16_t(b8) * b8;
}
__attribute__((weak))
void five_bit_hd_gamma_bitshift(
uint8_t r8, uint8_t g8, uint8_t b8,
uint8_t* out_r8,
uint8_t* out_g8,
uint8_t* out_b8,
uint8_t* out_power_5bit) {
// Step 1: Gamma Correction
uint16_t r16, g16, b16;
five_bit_hd_gamma_function(r8, g8, b8, &r16, &g16, &b16);
// Step 2: Initialize 5-bit brightness.
// Note: we only get 5 levels of brightness
uint8_t v8 = 31;
uint16_t nominator = 1;
uint16_t denominator = 1;
const uint16_t r16_const = r16;
const uint16_t g16_const = g16;
const uint16_t b16_const = b16;
// Step 3: Bit Shifting Loop, can probably replaced with a
// single pass bit-twiddling hack.
do {
{
uint32_t next_r16 = r16 * 31 / 15;
uint32_t next_g16 = g16 * 31 / 15;
uint32_t next_b16 = b16 * 31 / 15;
if (next_r16 > 0xffff) {
break;
}
if (next_g16 > 0xffff) {
break;
}
if (next_b16 > 0xffff) {
break;
}
nominator = nominator * 31;
denominator = denominator * 15;
v8 = v8 >> 1;
r16 = next_r16;
g16 = next_g16;
b16 = next_b16;
}
{
uint32_t next_r16 = r16 * 15 / 7;
uint32_t next_g16 = g16 * 15 / 7;
uint32_t next_b16 = b16 * 15 / 7;
if (next_r16 > 0xffff) {
break;
}
if (next_g16 > 0xffff) {
break;
}
if (next_b16 > 0xffff) {
break;
}
nominator = nominator * 15;
denominator = denominator * 7;
v8 = v8 >> 1;
r16 = next_r16;
g16 = next_g16;
b16 = next_b16;
}
{
uint32_t next_r16 = r16 * 7 / 3;
uint32_t next_g16 = g16 * 7 / 3;
uint32_t next_b16 = b16 * 7 / 3;
if (next_r16 > 0xffff) {
break;
}
if (next_g16 > 0xffff) {
break;
}
if (next_b16 > 0xffff) {
break;
}
nominator = nominator * 7;
denominator = denominator * 3;
v8 = v8 >> 1;
r16 = next_r16;
g16 = next_g16;
b16 = next_b16;
}
{
uint32_t next_r16 = r16 * 3;
uint32_t next_g16 = g16 * 3;
uint32_t next_b16 = b16 * 3;
if (next_r16 > 0xffff) {
break;
}
if (next_g16 > 0xffff) {
break;
}
if (next_b16 > 0xffff) {
break;
}
nominator = nominator * 3;
v8 = v8 >> 1;
r16 = next_r16;
g16 = next_g16;
b16 = next_b16;
}
} while(false);
r16 = r16_const * nominator / denominator;
g16 = g16_const * nominator / denominator;
b16 = b16_const * nominator / denominator;
// protect against overflow
if (r16 > 0xffff) {
r16 = 0xffff;
}
if (g16 > 0xffff) {
g16 = 0xffff;
}
if (b16 > 0xffff) {
b16 = 0xffff;
}
// Step 4: Conversion Back to 8-bit.
uint8_t r8_final = (r8 == 255 && uint8_t(r16 >> 8) >= 254) ? 255 : uint8_t(r16 >> 8);
uint8_t g8_final = (g8 == 255 && uint8_t(g16 >> 8) >= 254) ? 255 : uint8_t(g16 >> 8);
uint8_t b8_final = (b8 == 255 && uint8_t(b16 >> 8) >= 254) ? 255 : uint8_t(b16 >> 8);
if (v8 == 1) {
// Linear tuning for the lowest possible brightness. x=y until
// the intersection point at 9.
if (r8 < 9 && r16 > 0) {
r8_final = r8;
}
if (g8 < 9 && g16 > 0) {
g8_final = g8;
}
if (b8 < 9 && b16 > 0) {
b8_final = b8;
}
}
// Step 5: Output
*out_r8 = r8_final;
*out_g8 = g8_final;
*out_b8 = b8_final;
*out_power_5bit = v8;
}
FASTLED_NAMESPACE_END

54
src/five_bit_hd_gamma.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef _FIVE_BIT_HD_GAMMA_H_
#define _FIVE_BIT_HD_GAMMA_H_
#include "FastLED.h"
FASTLED_NAMESPACE_BEGIN
enum FiveBitGammaCorrectionMode {
kFiveBitGammaCorrectionMode_Null = 0,
kFiveBitGammaCorrectionMode_BitShift = 1
};
// Applies gamma correction for the RGBV(8, 8, 8, 5) color space, where
// the last byte is the brightness byte at 5 bits.
// To override this five_bit_hd_gamma_bitshift function just define
// your own version anywhere in your project.
// Example:
// FASTLED_NAMESPACE_BEGIN
// void five_bit_hd_gamma_bitshift(
// uint8_t r8, uint8_t g8, uint8_t b8,
// uint8_t* out_r8,
// uint8_t* out_g8,
// uint8_t* out_b8,
// uint8_t* out_power_5bit) {
// cout << "hello world\n";
// }
// FASTLED_NAMESPACE_END
void five_bit_hd_gamma_bitshift(
uint8_t r8, uint8_t g8, uint8_t b8,
uint8_t* out_r8,
uint8_t* out_g8,
uint8_t* out_b8,
uint8_t* out_power_5bit) __attribute__((weak));
// Simple gamma correction function that converts from
// 8-bit color component and converts it to gamma corrected 16-bit
// color component. Fast and no memory overhead!
// To override this function just define your own version
// anywhere in your project.
// Example:
// FASTLED_NAMESPACE_BEGIN
// void five_bit_hd_gamma_function(
// uint8_t r8, uint8_t g8, uint8_t b8,
// uint16_t* r16, uint16_t* g16, uint16_t* b16) {
// cout << "hello world\n";
// }
// FASTLED_NAMESPACE_END
void five_bit_hd_gamma_function(
uint8_t r8, uint8_t g8, uint8_t b8,
uint16_t* r16, uint16_t* g16, uint16_t* b16) __attribute__((weak));
FASTLED_NAMESPACE_END
#endif // _FIVE_BIT_HD_GAMMA_H_