From a36b331703179ac5b7cad3a02ea8f8c0fa267ac4 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 29 Sep 2023 14:10:35 -0700 Subject: [PATCH 1/6] Implements 5bit hd gamma correction using the bit shift method for APA102 and SK9822 chipsets --- ci/ci-compile | 2 +- examples/Apa102HD/Apa102HD.ino | 47 +++++++ src/FastLED.h | 10 +- src/chipsets.h | 216 +++++++++++++++++++++++---------- src/five_bit_hd_gamma.cpp | 162 +++++++++++++++++++++++++ src/five_bit_hd_gamma.h | 54 +++++++++ 6 files changed, 424 insertions(+), 67 deletions(-) create mode 100644 examples/Apa102HD/Apa102HD.ino create mode 100644 src/five_bit_hd_gamma.cpp create mode 100644 src/five_bit_hd_gamma.h diff --git a/ci/ci-compile b/ci/ci-compile index a7a21f2c..2bc3fd3d 100755 --- a/ci/ci-compile +++ b/ci/ci-compile @@ -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"} diff --git a/examples/Apa102HD/Apa102HD.ino b/examples/Apa102HD/Apa102HD.ino new file mode 100644 index 00000000..1cf3b201 --- /dev/null +++ b/examples/Apa102HD/Apa102HD.ino @@ -0,0 +1,47 @@ +/// @file Apa102HD.ino +/// @brief Example showing how to use the APA102HD gamma correction. +/// @example Apa102HD.ino + +#include +#include + +#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(leds_hd, NUM_LEDS); + FastLED.addLeds(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"); + } +} + diff --git a/src/FastLED.h b/src/FastLED.h index 4cc2e55c..aaf41095 100644 --- a/src/FastLED.h +++ b/src/FastLED.h @@ -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 c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case DOTSTAR: case APA102: { static APA102Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case APA102HD: { static APA102ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822: { static SK9822Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case SK9822HD: { static SK9822ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } } } @@ -289,7 +293,9 @@ public: case P9813: { static P9813Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case DOTSTAR: case APA102: { static APA102Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case APA102HD: { static APA102ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822: { static SK9822Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case SK9822HD: { static SK9822ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } } } @@ -304,7 +310,9 @@ public: case P9813: { static P9813Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case DOTSTAR: case APA102: { static APA102Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case APA102HD: { static APA102ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822: { static SK9822Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case SK9822HD: { static SK9822ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } } } diff --git a/src/chipsets.h b/src/chipsets.h index 2d08f723..13f058f3 100644 --- a/src/chipsets.h +++ b/src/chipsets.h @@ -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 +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 { typedef SPIOutput 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 { #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 & 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& 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(brightness); + } + // Legacy showPixels implementation. + inline void showPixelsDefault(PixelController & 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 & 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( + (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 -class SK9822Controller : public CPixelLEDController { - typedef SPIOutput 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 & 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 +> { }; diff --git a/src/five_bit_hd_gamma.cpp b/src/five_bit_hd_gamma.cpp new file mode 100644 index 00000000..0fb8eb2a --- /dev/null +++ b/src/five_bit_hd_gamma.cpp @@ -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 diff --git a/src/five_bit_hd_gamma.h b/src/five_bit_hd_gamma.h new file mode 100644 index 00000000..1c7cc66c --- /dev/null +++ b/src/five_bit_hd_gamma.h @@ -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_ From 73192ff41ba5ec8577cff9184714172e814e649f Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 29 Sep 2023 14:10:35 -0700 Subject: [PATCH 2/6] Implements 5bit hd gamma correction using the bit shift method for APA102 and SK9822 chipsets --- src/five_bit_hd_gamma.cpp | 155 ++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 91 deletions(-) diff --git a/src/five_bit_hd_gamma.cpp b/src/five_bit_hd_gamma.cpp index 0fb8eb2a..4476ea56 100644 --- a/src/five_bit_hd_gamma.cpp +++ b/src/five_bit_hd_gamma.cpp @@ -1,6 +1,10 @@ #include "FastLED.h" #include "five_bit_hd_gamma.h" +#ifndef FASTLED_FIVE_BIT_HD_GAMMA_LOW_END_LINEAR_RAMP +#define FASTLED_FIVE_BIT_HD_GAMMA_LOW_END_LINEAR_RAMP 1 +#endif + FASTLED_NAMESPACE_BEGIN @@ -31,113 +35,81 @@ void five_bit_hd_gamma_bitshift( 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; + const uint32_t r16_const = r16; + const uint32_t g16_const = g16; + const uint32_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; + // Note that to avoid slow divisions, we multiply the max_value + // by the denominator. + uint32_t max_value = 0xfffful * 15; + if (r16_const * 31 > max_value) { + break; } - { - 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; + if (g16_const * 31 > max_value) { + break; } - { - 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; + if (b16_const * 31 > max_value) { + break; } - { - 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; + nominator = 31; + denominator = 15; + v8 = 15; + + max_value = 0xfffful * 15 * 7; + if (r16_const * 31 * 15 > max_value) { + break; } + if (g16_const * 31 * 15 > max_value) { + break; + } + if (b16_const * 31 * 15 > max_value) { + break; + } + nominator = 31 * 15; + denominator = 15 * 7; + v8 = 7; + + max_value = 0xfffful * 15 * 7 * 3; + if (r16_const * 31 * 15 * 7 > max_value) { + break; + } + if (g16_const * 31 * 15 * 7 > max_value) { + break; + } + if (b16_const * 31 * 15 * 7 > max_value) { + break; + } + nominator = 31 * 15 * 7; + denominator = 15 * 7 * 3; + v8 = 3; + + max_value = 0xfffful * 15 * 7 * 3; + if (r16_const * 31 * 15 * 7 * 3 > max_value) { + break; + } + if (g16_const * 31 * 15 * 7 * 3 > max_value) { + break; + } + if (b16_const * 31 * 15 * 7 * 3 > max_value) { + break; + } + nominator = 31 * 15 * 7 * 3; + v8 = 1; } 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; - } + r16 = uint16_t(r16_const * nominator / denominator); + g16 = uint16_t(g16_const * nominator / denominator); + b16 = uint16_t(b16_const * nominator / denominator); // 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 FASTLED_FIVE_BIT_HD_GAMMA_LOW_END_LINEAR_RAMP == 1 if (v8 == 1) { // Linear tuning for the lowest possible brightness. x=y until // the intersection point at 9. @@ -151,6 +123,7 @@ void five_bit_hd_gamma_bitshift( b8_final = b8; } } +#endif // Step 5: Output *out_r8 = r8_final; From 26cd9f1d7d39a3475828554daf11352780efdbc2 Mon Sep 17 00:00:00 2001 From: zackees Date: Thu, 12 Oct 2023 17:40:40 -0700 Subject: [PATCH 3/6] adds DOTSTARHD --- examples/Apa102HD/Apa102HD.ino | 1 + src/FastLED.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/examples/Apa102HD/Apa102HD.ino b/examples/Apa102HD/Apa102HD.ino index 1cf3b201..430f17ca 100644 --- a/examples/Apa102HD/Apa102HD.ino +++ b/examples/Apa102HD/Apa102HD.ino @@ -27,6 +27,7 @@ void setup() { delay(500); // power-up safety delay FastLED.addLeds(leds_hd, NUM_LEDS); FastLED.addLeds(leds, NUM_LEDS); + FastLED.addLeds(leds_hd, NUM_LEDS); // compile test. } void loop() { diff --git a/src/FastLED.h b/src/FastLED.h index aaf41095..92ba4d9d 100644 --- a/src/FastLED.h +++ b/src/FastLED.h @@ -89,6 +89,7 @@ enum ESPIChipsets { SK9822, ///< SK9822 LED chipset SK9822HD, ///< SK9822 LED chipset with 5-bit gamma correction DOTSTAR, ///< APA102 LED chipset alias + DOTSTARHD, ///< APA102HD LED chipset alias APA102HD, ///< APA102 LED chipset with 5-bit gamma correction }; @@ -276,6 +277,7 @@ public: case P9813: { static P9813Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case DOTSTAR: case APA102: { static APA102Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case DOTSTARHD: case APA102HD: { static APA102ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822: { static SK9822Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822HD: { static SK9822ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } @@ -293,6 +295,7 @@ public: case P9813: { static P9813Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case DOTSTAR: case APA102: { static APA102Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case DOTSTARHD: case APA102HD: { static APA102ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822: { static SK9822Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822HD: { static SK9822ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } @@ -310,6 +313,7 @@ public: case P9813: { static P9813Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case DOTSTAR: case APA102: { static APA102Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } + case DOTSTARHD: case APA102HD: { static APA102ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822: { static SK9822Controller c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } case SK9822HD: { static SK9822ControllerHD c; return addLeds(&c, data, nLedsOrOffset, nLedsIfOffset); } From ba109e900905c1cb23454b83003b1e1aba7b3033 Mon Sep 17 00:00:00 2001 From: zackees Date: Thu, 12 Oct 2023 18:13:48 -0700 Subject: [PATCH 4/6] improve example --- examples/Apa102HD/Apa102HD.ino | 77 ++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/examples/Apa102HD/Apa102HD.ino b/examples/Apa102HD/Apa102HD.ino index 430f17ca..807f6540 100644 --- a/examples/Apa102HD/Apa102HD.ino +++ b/examples/Apa102HD/Apa102HD.ino @@ -1,48 +1,73 @@ /// @file Apa102HD.ino /// @brief Example showing how to use the APA102HD gamma correction. +/// +/// We will draw a linear ramp between APA102HD which does it's +/// own gamma correction at the driver level, and the software +/// version of gamma correction, which used to be the status quo. +/// +/// Why do we love gamma correction? It produces a better picture +/// that matches how our eye sees light. It's the standard lighting +/// method for your monitor or TVs for example and gives these +/// devices the range to produce very bright and very dim lights. /// @example Apa102HD.ino #include #include +#include -#define NUM_LEDS 20 +#define NUM_LEDS 256 +// uint8_t DATA_PIN, uint8_t CLOCK_PIN, +#define STRIP_0_DATA_PIN 1 +#define STRIP_0_CLOCK_PIN 2 +#define STRIP_1_DATA_PIN 3 +#define STRIP_1_CLOCK_PIN 4 -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; +// This is the regular gamma correction function that we used to have +// to do. It's used here to showcase the difference between APA102HD +// mode which does the gamma correction for you. +CRGB software_gamma(const CRGB& in) { + CRGB out; + // dim8_raw are the old gamma correction functions. + out.r = dim8_raw(in.r); + out.g = dim8_raw(in.g); + out.b = dim8_raw(in.b); + return out; } void setup() { delay(500); // power-up safety delay - FastLED.addLeds(leds_hd, NUM_LEDS); - FastLED.addLeds(leds, NUM_LEDS); - FastLED.addLeds(leds_hd, NUM_LEDS); // compile test. + // Two strips of LEDs, one in HD mode, one in software gamma mode. + FastLED.addLeds(leds_hd, NUM_LEDS); + FastLED.addLeds(leds, NUM_LEDS); +} + +uint8_t wrap_8bit(int i) { + // Module % operator here wraps a large "i" so that it is + // always in [0, 255] range when returned. For example, if + // "i" is 256, then this will return 0. If "i" is 257 + // then this will return 1. No matter how big the "i" is, the + // output range will always be [0, 255] + return i % 256; } void loop() { - uint32_t now = millis(); - uint32_t t = now / 100; + // Draw a a linear ramp of brightnesses, showcasing the difference + // between the two gamma correction modes. APA102HD should have far + // greater range of brightnesses. Gamma correction mode in software + // will have about 25% of the dimmest lights truncated to 0 brightness! 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"); + // Make sure the i value is always in 8 bit range. + uint8_t brightness = wrap_8bit(i); + CRGB c(brightness, brightness, brightness); // Just make a shade of white. + leds_hd[i] = c; // The APA102HD leds are set to ungamma corrected values. + CRGB c_gamma_corrected = software_gamma(c); + leds[i] = c_gamma_corrected; // Set the software gamma corrected + // values to the other strip. } + FastLED.show(); // All leds are now written out. + delay(8); // Wait 8 milliseconds until the next frame. } From e0c509e16a28f6f26d501c43c95ab5ae5f0c04bf Mon Sep 17 00:00:00 2001 From: zackees Date: Thu, 12 Oct 2023 18:17:19 -0700 Subject: [PATCH 5/6] fix typo nominator -> numerator --- src/five_bit_hd_gamma.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/five_bit_hd_gamma.cpp b/src/five_bit_hd_gamma.cpp index 4476ea56..f908ff09 100644 --- a/src/five_bit_hd_gamma.cpp +++ b/src/five_bit_hd_gamma.cpp @@ -33,7 +33,7 @@ void five_bit_hd_gamma_bitshift( // Note: we only get 5 levels of brightness uint8_t v8 = 31; - uint16_t nominator = 1; + uint16_t numerator = 1; uint16_t denominator = 1; const uint32_t r16_const = r16; const uint32_t g16_const = g16; @@ -54,7 +54,7 @@ void five_bit_hd_gamma_bitshift( if (b16_const * 31 > max_value) { break; } - nominator = 31; + numerator = 31; denominator = 15; v8 = 15; @@ -68,7 +68,7 @@ void five_bit_hd_gamma_bitshift( if (b16_const * 31 * 15 > max_value) { break; } - nominator = 31 * 15; + numerator = 31 * 15; denominator = 15 * 7; v8 = 7; @@ -82,7 +82,7 @@ void five_bit_hd_gamma_bitshift( if (b16_const * 31 * 15 * 7 > max_value) { break; } - nominator = 31 * 15 * 7; + numerator = 31 * 15 * 7; denominator = 15 * 7 * 3; v8 = 3; @@ -96,13 +96,13 @@ void five_bit_hd_gamma_bitshift( if (b16_const * 31 * 15 * 7 * 3 > max_value) { break; } - nominator = 31 * 15 * 7 * 3; + numerator = 31 * 15 * 7 * 3; v8 = 1; } while(false); - r16 = uint16_t(r16_const * nominator / denominator); - g16 = uint16_t(g16_const * nominator / denominator); - b16 = uint16_t(b16_const * nominator / denominator); + r16 = uint16_t(r16_const * numerator / denominator); + g16 = uint16_t(g16_const * numerator / denominator); + b16 = uint16_t(b16_const * numerator / denominator); // Step 4: Conversion Back to 8-bit. uint8_t r8_final = (r8 == 255 && uint8_t(r16 >> 8) >= 254) ? 255 : uint8_t(r16 >> 8); From 1f2f3da6e19b22cb650979efcf659871a78736a2 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 13 Oct 2023 00:27:48 -0700 Subject: [PATCH 6/6] update example with more explaination --- examples/Apa102HD/Apa102HD.ino | 46 ++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/examples/Apa102HD/Apa102HD.ino b/examples/Apa102HD/Apa102HD.ino index 807f6540..76138a67 100644 --- a/examples/Apa102HD/Apa102HD.ino +++ b/examples/Apa102HD/Apa102HD.ino @@ -1,21 +1,38 @@ /// @file Apa102HD.ino /// @brief Example showing how to use the APA102HD gamma correction. /// -/// We will draw a linear ramp between APA102HD which does it's -/// own gamma correction at the driver level, and the software -/// version of gamma correction, which used to be the status quo. +/// In this example we compare two strips of LEDs. +/// One strip is in HD mode, the other is in software gamma mode. /// -/// Why do we love gamma correction? It produces a better picture -/// that matches how our eye sees light. It's the standard lighting -/// method for your monitor or TVs for example and gives these -/// devices the range to produce very bright and very dim lights. -/// @example Apa102HD.ino +/// Each strip is a linear ramp of brightnesses, from 0 to 255. +/// Showcasing all the different brightnesses. +/// +/// Why do we love gamma correction? Gamma correction more closely +/// matches how humans see light. Led values are measured in fractions +/// of max power output (1/255, 2/255, etc.), while humans see light +/// in a logarithmic way. Gamma correction converts to this eye friendly +/// curve. Gamma correction wants a LED with a high bit depth. The APA102 +/// gives us the standard 3 components (red, green, blue) with 8 bits each, it +/// *also* has a 5 bit brightness component. This gives us a total of 13 bits, +/// which allows us to achieve a higher dynamic range. This means deeper fades. +/// +/// Example: +/// CRGB leds[NUM_LEDS] = {0}; +/// void setup() { +/// FastLED.addLeds< +/// APA102HD, // <--- This selects HD mode. +/// STRIP_0_DATA_PIN, +/// STRIP_0_CLOCK_PIN, +/// RGB +/// >(leds, NUM_LEDS); +/// } + #include #include #include -#define NUM_LEDS 256 +#define NUM_LEDS 20 // uint8_t DATA_PIN, uint8_t CLOCK_PIN, #define STRIP_0_DATA_PIN 1 #define STRIP_0_CLOCK_PIN 2 @@ -54,15 +71,12 @@ uint8_t wrap_8bit(int i) { } void loop() { - // Draw a a linear ramp of brightnesses, showcasing the difference - // between the two gamma correction modes. APA102HD should have far - // greater range of brightnesses. Gamma correction mode in software - // will have about 25% of the dimmest lights truncated to 0 brightness! + // Draw a a linear ramp of brightnesses to showcase the difference between + // the HD and non-HD mode. for (int i = 0; i < NUM_LEDS; i++) { - // Make sure the i value is always in 8 bit range. - uint8_t brightness = wrap_8bit(i); + uint8_t brightness = map(i, 0, NUM_LEDS - 1, 0, 255); CRGB c(brightness, brightness, brightness); // Just make a shade of white. - leds_hd[i] = c; // The APA102HD leds are set to ungamma corrected values. + leds_hd[i] = c; // The APA102HD leds do their own gamma correction. CRGB c_gamma_corrected = software_gamma(c); leds[i] = c_gamma_corrected; // Set the software gamma corrected // values to the other strip.