From a36b331703179ac5b7cad3a02ea8f8c0fa267ac4 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 29 Sep 2023 14:10:35 -0700 Subject: [PATCH] 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_